# core/converters/search.index.converter.ts

> 112 lines of code and 27 definitions.

Tree: Bane's Lab Build Scripts
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/build-scripts#file-build-scripts-core-converters-search-index-converter-ts
Source text: https://banes-lab.com/source/build-scripts/core/converters/search.index.converter.ts.txt

Listed in [core/converters](https://banes-lab.com/api/source/build-scripts/core/converters.md), after [core/converters/search.converter.ts](https://banes-lab.com/source/build-scripts/core/converters/search.converter.ts.md) and before [core/converters/section.converter.ts](https://banes-lab.com/source/build-scripts/core/converters/section.converter.ts.md).

## Definitions

- `kindLeaves` (lexical_declaration, line 79)
- `rowParts` (lexical_declaration, line 58)
- `shardLeaf` (lexical_declaration, line 43)
- `searchLeaves` (lexical_declaration, line 101, exported)
- `SEARCH_REF` (lexical_declaration, line 19)
- `KIND_REF` (lexical_declaration, line 20)
- `REF_SEPARATOR` (lexical_declaration, line 21)
- `COLUMNS` (lexical_declaration, line 22)
- `POSTING` (lexical_declaration, line 23)
- `postingsOf` (lexical_declaration, line 25)
- `counts` (lexical_declaration, line 28)
- `rowOf` (lexical_declaration, line 39)
- `postings` (lexical_declaration, line 50)
- `number` (lexical_declaration, line 61)
- `{ entries, kind }` (lexical_declaration, line 80)
- `prefixes` (lexical_declaration, line 81)
- `shards` (lexical_declaration, line 82)
- `ref` (lexical_declaration, line 83)
- `addresses` (lexical_declaration, line 85)
- `rows` (lexical_declaration, line 88)
- `whole` (lexical_declaration, line 89)
- `parted` (lexical_declaration, line 90)
- `parts` (lexical_declaration, line 91)
- `data` (lexical_declaration, line 92)
- `indexes` (lexical_declaration, line 106, exported)
- `title` (lexical_declaration, line 112, exported)
- `head` (lexical_declaration, line 113, exported)

## Contained in

- [core/converters](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-converters.md)

## Uses

- [core/resolvers/catalog.resolver.ts](https://banes-lab.com/source/build-scripts/core/resolvers/catalog.resolver.ts.md)

## Linked from

- [core/converters](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-converters.md)
- [core/resolvers](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-resolvers.md)

## Source

```typescript
import {
    CATALOG_FILE_BUDGET,
    INDEX_PART_BUDGET,
    INDEX_PART_MARK,
    SEARCH_KIND,
} from "#configuration/constants/catalog.constants";
import type { Leaf, Linker } from "#types/catalog.types";
import {
    SEARCH_INDEX_TITLE,
    indexPartTitle,
    searchKindTitle,
    searchShardTitle,
} from "#configuration/strings/catalog.strings";
import type { SearchEntry, SearchKind, SearchRules } from "#types/search.types";
import { indexPart, searchIndex, searchKindIndex, searchShard } from "#core/resolvers/catalog.resolver";
import { packedParts, prefixShards, serialize } from "#core/stores/catalog.store";
import { Buffer } from "node:buffer";

const SEARCH_REF = "api:search";
const KIND_REF = "api:search/";
const REF_SEPARATOR = "/";
const COLUMNS = ["ref", "title", "position"] as const;
const POSTING = ["row", "count"] as const;

const postingsOf = function postingsOf(entries: readonly SearchEntry[]): ReadonlyMap<string, [number, number][]> {
    const postings = new Map<string, [number, number][]>();
    for (const [row, entry] of entries.entries()) {
        const counts = new Map<string, number>();
        for (const word of entry.words) {
            counts.set(word, (counts.get(word) ?? 0) + 1);
        }
        for (const [word, count] of counts) {
            postings.set(word, [...(postings.get(word) ?? []), [row, count]]);
        }
    }
    return postings;
};

const rowOf = function rowOf(entry: SearchEntry): readonly unknown[] {
    return [entry.identity.ref, entry.identity.title, entry.position];
};

const shardLeaf = function shardLeaf(
    kind: string,
    prefix: string,
    words: readonly (readonly [string, [number, number][]])[],
): Leaf {
    const ref = KIND_REF + kind + REF_SEPARATOR + prefix;
    const title = searchShardTitle(kind, prefix);
    const postings = Object.fromEntries(words.toSorted(([left], [right]) => left.localeCompare(right)));
    return {
        data: { kind, posting: POSTING, postings, prefix, ref, title },
        identity: { address: searchShard(kind, prefix), href: null, kind: SEARCH_KIND, ref, summary: null, title },
        markdown: null,
    };
};

const rowParts = function rowParts(kind: string, rows: readonly (readonly unknown[])[]): readonly Leaf[] {
    const parts = packedParts(rows, INDEX_PART_BUDGET);
    return parts.map((part, index): Leaf => {
        const number = index + 1;
        const ref = KIND_REF + kind + REF_SEPARATOR + INDEX_PART_MARK + String(number);
        const title = indexPartTitle(searchKindTitle(kind), number, parts.length);
        return {
            data: { columns: COLUMNS, kind, part: number, ref, rows: part, title, total: parts.length },
            identity: {
                address: indexPart(searchKindIndex(kind), String(number)),
                href: null,
                kind: SEARCH_KIND,
                ref,
                summary: null,
                title,
            },
            markdown: null,
        };
    });
};

const kindLeaves = function kindLeaves(search: SearchKind, linker: Linker): readonly Leaf[] {
    const { entries, kind } = search;
    const prefixes = prefixShards([...postingsOf(entries).entries()], INDEX_PART_BUDGET);
    const shards = [...prefixes.entries()].map(([prefix, words]) => shardLeaf(kind, prefix, words));
    const ref = KIND_REF + kind;
    const title = searchKindTitle(kind);
    const addresses = Object.fromEntries(
        [...prefixes.keys()].map((prefix) => [prefix, linker.site + searchShard(kind, prefix).json]),
    );
    const rows = entries.map(rowOf);
    const whole = { columns: COLUMNS, kind, ref, rows, shards: addresses, title };
    const parted = Buffer.byteLength(serialize(whole)) > CATALOG_FILE_BUDGET ? rowParts(kind, rows) : [];
    const parts = parted.map((part) => linker.site + part.identity.address.json);
    const data = parted.length === 0 ? whole : { ...whole, parts, rows: [] };
    const head: Leaf = {
        data,
        identity: { address: searchKindIndex(kind), href: null, kind: SEARCH_KIND, ref, summary: null, title },
        markdown: null,
    };
    return [head, ...parted, ...shards];
};

export const searchLeaves = function searchLeaves(
    kinds: readonly SearchKind[],
    rules: SearchRules,
    linker: Linker,
): readonly Leaf[] {
    const indexes = Object.fromEntries(
        kinds.map(({ entries, kind }) => [
            kind,
            { count: entries.length, json: linker.site + searchKindIndex(kind).json },
        ]),
    );
    const title = SEARCH_INDEX_TITLE;
    const head: Leaf = {
        data: { kinds: indexes, posting: POSTING, ref: SEARCH_REF, rules, title },
        identity: { address: searchIndex(), href: null, kind: SEARCH_KIND, ref: SEARCH_REF, summary: null, title },
        markdown: null,
    };
    return [head, ...kinds.flatMap((kind) => kindLeaves(kind, linker))];
};
```
