@statewalker/indexer-ngram 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +85 -2
- package/dist/access.d.ts +18 -0
- package/dist/access.d.ts.map +1 -0
- package/dist/access.js +23 -0
- package/dist/access.js.map +1 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +21 -0
- package/dist/config.js.map +1 -0
- package/dist/grep.d.ts +13 -0
- package/dist/grep.d.ts.map +1 -0
- package/dist/grep.js +22 -0
- package/dist/grep.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/package.json +51 -3
- package/src/access.ts +41 -0
- package/src/config.ts +16 -0
- package/src/grep.ts +29 -0
- package/src/index.ts +14 -0
- package/src/types.ts +75 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mikhail Kotelnikov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,86 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @statewalker/indexer-ngram
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## What it is
|
|
4
|
+
|
|
5
|
+
The n-gram / regex modality contract for `@statewalker/indexer-api`: the block, query and result
|
|
6
|
+
types, the `NGRAM_TYPE` discriminator, config helpers, a typed access handle (`newNgramAccess`) and
|
|
7
|
+
a `grep` helper. It holds no engine; a backend registers a sub-index of type `"ngram"` that
|
|
8
|
+
implements `NgramIndex`. `@statewalker/indexer-sqlite` (`newSqliteNgramProvider()`) is such a backend.
|
|
9
|
+
|
|
10
|
+
## Why exact-match search needs its own modality
|
|
11
|
+
|
|
12
|
+
Full-text search drops punctuation and stems words, so it cannot find `fooBar(`, `TODO:` or a
|
|
13
|
+
regular expression. Grep needs a modality that matches the original text exactly and reports where
|
|
14
|
+
each match sits (offsets and line numbers). Backends implement that behind the types in this
|
|
15
|
+
package, so callers write against the contract and fuse grep hits with full-text and vector hits in
|
|
16
|
+
one `SearchRequest`.
|
|
17
|
+
|
|
18
|
+
## How to use
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { grep, newNgramAccess, setNgramConfig } from "@statewalker/indexer-ngram";
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- `setNgramConfig(params, name, { store? })` declares the sub-index in `CreateIndexParams`.
|
|
25
|
+
- `newNgramAccess(name).get(index)` returns the typed `NgramIndex` for ingestion
|
|
26
|
+
(`addDocument`, ...) and `search`.
|
|
27
|
+
- `grep(index, name, pattern, options?)` runs a search and returns `{ hits, truncated }`.
|
|
28
|
+
|
|
29
|
+
## Examples
|
|
30
|
+
|
|
31
|
+
Config, ingest, grep:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { grep, newNgramAccess, setNgramConfig } from "@statewalker/indexer-ngram";
|
|
35
|
+
|
|
36
|
+
const params = { name: "code" /* ... */ };
|
|
37
|
+
setNgramConfig(params, "grep", {}); // backend picks the store
|
|
38
|
+
|
|
39
|
+
// `indexer` is a backend that handles type "ngram"; `index` came from it.
|
|
40
|
+
const ngram = newNgramAccess("grep");
|
|
41
|
+
await ngram.get(index).addDocument([
|
|
42
|
+
{ path: "/src/a.ts", blockId: "0", content: "const x = fooBar(1); // TODO: fix" },
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const { hits, truncated } = await grep(index, "grep", "TODO:");
|
|
46
|
+
// hits[0] -> { path: "/src/a.ts", blockId: "0", score: 1, count: 1,
|
|
47
|
+
// matches: [{ start: 24, end: 29, line: 1 }] }
|
|
48
|
+
|
|
49
|
+
const re = await grep(index, "grep", "foo[A-Z]\\w+\\(", { regex: true, topK: 20 });
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Fused with other modalities in one request:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import type { SearchRequest } from "@statewalker/indexer-api";
|
|
56
|
+
|
|
57
|
+
const request: SearchRequest = { topK: 10 };
|
|
58
|
+
ngram.setQuery(request, { pattern: "fooBar(", caseSensitive: false });
|
|
59
|
+
// ... set the full-text / vector sub-queries on the same request ...
|
|
60
|
+
for await (const entry of index.search(request)) {
|
|
61
|
+
const hit = ngram.getResult(entry); // NgramResult | undefined
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Internals
|
|
66
|
+
|
|
67
|
+
- **Score is the match count.** `NgramResult.score === count`; `matches` lists every match found in
|
|
68
|
+
the block.
|
|
69
|
+
- **Matches never cross blocks.** A pattern spanning two blocks does not match.
|
|
70
|
+
- **Offsets.** `start`/`end` are UTF-16 code-unit offsets into the original block text, `end`
|
|
71
|
+
exclusive; `line` is 1-based.
|
|
72
|
+
- **Case.** `caseSensitive` defaults to `true` (grep semantics).
|
|
73
|
+
- **Limits.** `topK` defaults to 100 and caps returned blocks; `topK: 0` returns no hits. A backend
|
|
74
|
+
may stop verifying candidates early (the SQLite backend's candidate budget is 10,000). Either cut
|
|
75
|
+
sets `truncated: true`.
|
|
76
|
+
- **Regex dialect.** `regex: true` compiles a JavaScript regular expression without the `u` flag:
|
|
77
|
+
`.` matches one UTF-16 code unit and `\u{...}` is not a code-point escape. Patterns containing
|
|
78
|
+
`\p{` or `\P{` are rejected with a `SyntaxError`; Unicode-property escapes are not supported.
|
|
79
|
+
- **`truncated` travels as the generator's return value.** `NgramIndex.search` yields hits and
|
|
80
|
+
returns `{ truncated }`. A `for await` over `search` silently ignores truncation and shows
|
|
81
|
+
possibly incomplete results as complete; use `grep`, which reads the return value.
|
|
82
|
+
- **Dependencies.** Only `@statewalker/indexer-api`; no engine, no database package.
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT
|
package/dist/access.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { NgramConfig, NgramIndex, NgramQuery, NgramResult } from "./types.js";
|
|
2
|
+
import { CreateIndexParams, Index, SearchRequest, SearchResult } from "@statewalker/indexer-api";
|
|
3
|
+
//#region src/access.d.ts
|
|
4
|
+
/** User-facing typed bundle bound to one n-gram sub-index **name**. */
|
|
5
|
+
export interface NgramAccess {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
/** Throws if no sub-index named `name` is registered on `index`. */
|
|
8
|
+
get(index: Index): NgramIndex;
|
|
9
|
+
tryGet(index: Index): NgramIndex | undefined;
|
|
10
|
+
setConfig(params: CreateIndexParams, config: NgramConfig): void;
|
|
11
|
+
getConfig(params: CreateIndexParams): NgramConfig | undefined;
|
|
12
|
+
setQuery(request: SearchRequest, query: NgramQuery): void;
|
|
13
|
+
getQuery(request: SearchRequest): NgramQuery | undefined;
|
|
14
|
+
getResult(entry: SearchResult): NgramResult | undefined;
|
|
15
|
+
}
|
|
16
|
+
export declare function newNgramAccess(name: string): NgramAccess;
|
|
17
|
+
//#endregion
|
|
18
|
+
//# sourceMappingURL=access.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access.d.ts","names":[],"sources":["../src/access.ts"],"mappings":";;;;iBAWiB;WACN;;EAET,IAAI,OAAO,QAAQ;EACnB,OAAO,OAAO,QAAQ;EACtB,UAAU,QAAQ,mBAAmB,QAAQ;EAC7C,UAAU,QAAQ,oBAAoB;EACtC,SAAS,SAAS,eAAe,OAAO;EACxC,SAAS,SAAS,gBAAgB;EAClC,UAAU,OAAO,eAAe;;wBAGlB,eAAe,eAAe"}
|
package/dist/access.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { getNgramConfig, setNgramConfig } from "./config.js";
|
|
2
|
+
import { getSubQuery, getSubResult, setSubQuery } from "@statewalker/indexer-api";
|
|
3
|
+
//#region src/access.ts
|
|
4
|
+
function newNgramAccess(name) {
|
|
5
|
+
return {
|
|
6
|
+
name,
|
|
7
|
+
get(index) {
|
|
8
|
+
const binding = index.getBinding(name);
|
|
9
|
+
if (!binding) throw new Error(`No sub-index named "${name}" is registered on index "${index.name}"`);
|
|
10
|
+
return binding.index;
|
|
11
|
+
},
|
|
12
|
+
tryGet: (index) => index.getBinding(name)?.index,
|
|
13
|
+
setConfig: (params, config) => setNgramConfig(params, name, config),
|
|
14
|
+
getConfig: (params) => getNgramConfig(params, name),
|
|
15
|
+
setQuery: (request, query) => setSubQuery(request, name, query),
|
|
16
|
+
getQuery: (request) => getSubQuery(request, name),
|
|
17
|
+
getResult: (entry) => getSubResult(entry, name)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { newNgramAccess };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=access.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access.js","names":[],"sources":["../src/access.ts"],"sourcesContent":["import type {\n CreateIndexParams,\n Index,\n SearchRequest,\n SearchResult,\n} from \"@statewalker/indexer-api\";\nimport { getSubQuery, getSubResult, setSubQuery } from \"@statewalker/indexer-api\";\nimport { getNgramConfig, setNgramConfig } from \"./config.js\";\nimport type { NgramConfig, NgramIndex, NgramQuery, NgramResult } from \"./types.js\";\n\n/** User-facing typed bundle bound to one n-gram sub-index **name**. */\nexport interface NgramAccess {\n readonly name: string;\n /** Throws if no sub-index named `name` is registered on `index`. */\n get(index: Index): NgramIndex;\n tryGet(index: Index): NgramIndex | undefined;\n setConfig(params: CreateIndexParams, config: NgramConfig): void;\n getConfig(params: CreateIndexParams): NgramConfig | undefined;\n setQuery(request: SearchRequest, query: NgramQuery): void;\n getQuery(request: SearchRequest): NgramQuery | undefined;\n getResult(entry: SearchResult): NgramResult | undefined;\n}\n\nexport function newNgramAccess(name: string): NgramAccess {\n return {\n name,\n get(index: Index): NgramIndex {\n const binding = index.getBinding(name);\n if (!binding) {\n throw new Error(`No sub-index named \"${name}\" is registered on index \"${index.name}\"`);\n }\n return binding.index as NgramIndex;\n },\n tryGet: (index) => index.getBinding(name)?.index as NgramIndex | undefined,\n setConfig: (params, config) => setNgramConfig(params, name, config),\n getConfig: (params) => getNgramConfig(params, name),\n setQuery: (request, query) => setSubQuery<NgramQuery>(request, name, query),\n getQuery: (request) => getSubQuery<NgramQuery>(request, name),\n getResult: (entry) => getSubResult<NgramResult>(entry, name),\n };\n}\n"],"mappings":";;;AAuBA,SAAgB,eAAe,MAA2B;CACxD,OAAO;EACL;EACA,IAAI,OAA0B;GAC5B,MAAM,UAAU,MAAM,WAAW,IAAI;GACrC,IAAI,CAAC,SACH,MAAM,IAAI,MAAM,uBAAuB,KAAK,4BAA4B,MAAM,KAAK,EAAE;GAEvF,OAAO,QAAQ;EACjB;EACA,SAAS,UAAU,MAAM,WAAW,IAAI,CAAC,EAAE;EAC3C,YAAY,QAAQ,WAAW,eAAe,QAAQ,MAAM,MAAM;EAClE,YAAY,WAAW,eAAe,QAAQ,IAAI;EAClD,WAAW,SAAS,UAAU,YAAwB,SAAS,MAAM,KAAK;EAC1E,WAAW,YAAY,YAAwB,SAAS,IAAI;EAC5D,YAAY,UAAU,aAA0B,OAAO,IAAI;CAC7D;AACF"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { NgramConfig } from "./types.js";
|
|
2
|
+
import { CreateIndexParams } from "@statewalker/indexer-api";
|
|
3
|
+
//#region src/config.d.ts
|
|
4
|
+
/** Write an n-gram config for the sub-index named `name` into `params.subIndexes`. */
|
|
5
|
+
export declare function setNgramConfig(params: CreateIndexParams, name: string, config: NgramConfig): void;
|
|
6
|
+
/** Read the n-gram config for the sub-index named `name`, if any. */
|
|
7
|
+
export declare function getNgramConfig(params: CreateIndexParams, name: string): NgramConfig | undefined;
|
|
8
|
+
//#endregion
|
|
9
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","names":[],"sources":["../src/config.ts"],"mappings":";;;;wBAIgB,eAAe,QAAQ,mBAAmB,cAAc,QAAQ;;wBAMhE,eAAe,QAAQ,mBAAmB,eAAe"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NGRAM_TYPE } from "./types.js";
|
|
2
|
+
//#region src/config.ts
|
|
3
|
+
/** Write an n-gram config for the sub-index named `name` into `params.subIndexes`. */
|
|
4
|
+
function setNgramConfig(params, name, config) {
|
|
5
|
+
if (!params.subIndexes) params.subIndexes = {};
|
|
6
|
+
params.subIndexes[name] = {
|
|
7
|
+
type: NGRAM_TYPE,
|
|
8
|
+
...config
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** Read the n-gram config for the sub-index named `name`, if any. */
|
|
12
|
+
function getNgramConfig(params, name) {
|
|
13
|
+
const entry = params.subIndexes?.[name];
|
|
14
|
+
if (!entry || entry.type !== "ngram") return void 0;
|
|
15
|
+
const { type: _type, ...rest } = entry;
|
|
16
|
+
return rest;
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
export { getNgramConfig, setNgramConfig };
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","names":[],"sources":["../src/config.ts"],"sourcesContent":["import type { CreateIndexParams } from \"@statewalker/indexer-api\";\nimport { NGRAM_TYPE, type NgramConfig } from \"./types.js\";\n\n/** Write an n-gram config for the sub-index named `name` into `params.subIndexes`. */\nexport function setNgramConfig(params: CreateIndexParams, name: string, config: NgramConfig): void {\n if (!params.subIndexes) params.subIndexes = {};\n params.subIndexes[name] = { type: NGRAM_TYPE, ...config };\n}\n\n/** Read the n-gram config for the sub-index named `name`, if any. */\nexport function getNgramConfig(params: CreateIndexParams, name: string): NgramConfig | undefined {\n const entry = params.subIndexes?.[name];\n if (!entry || entry.type !== NGRAM_TYPE) return undefined;\n const { type: _type, ...rest } = entry;\n return rest as NgramConfig;\n}\n"],"mappings":";;;AAIA,SAAgB,eAAe,QAA2B,MAAc,QAA2B;CACjG,IAAI,CAAC,OAAO,YAAY,OAAO,aAAa,CAAC;CAC7C,OAAO,WAAW,QAAQ;EAAE,MAAM;EAAY,GAAG;CAAO;AAC1D;;AAGA,SAAgB,eAAe,QAA2B,MAAuC;CAC/F,MAAM,QAAQ,OAAO,aAAa;CAClC,IAAI,CAAC,SAAS,MAAM,SAAA,SAAqB,OAAO,KAAA;CAChD,MAAM,EAAE,MAAM,OAAO,GAAG,SAAS;CACjC,OAAO;AACT"}
|
package/dist/grep.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { NgramQuery, NgramResult } from "./types.js";
|
|
2
|
+
import { Index } from "@statewalker/indexer-api";
|
|
3
|
+
//#region src/grep.d.ts
|
|
4
|
+
export type GrepOptions = Omit<NgramQuery, "pattern">;
|
|
5
|
+
export interface GrepResult {
|
|
6
|
+
hits: NgramResult[];
|
|
7
|
+
/** `true` when the backend stopped early (candidate budget or `topK`); `hits` may be incomplete. */
|
|
8
|
+
truncated: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** Run `pattern` on the n-gram sub-index `name` of `index` and collect every hit plus the truncation flag. */
|
|
11
|
+
export declare function grep(index: Index, name: string, pattern: string, options?: GrepOptions): Promise<GrepResult>;
|
|
12
|
+
//#endregion
|
|
13
|
+
//# sourceMappingURL=grep.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grep.d.ts","names":[],"sources":["../src/grep.ts"],"mappings":";;;YAIY,cAAc,KAAK;iBAEd;EACf,MAAM;;EAEN;;;wBAIoB,KACpB,OAAO,OACP,cACA,iBACA,UAAS,cACR,QAAQ"}
|
package/dist/grep.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { newNgramAccess } from "./access.js";
|
|
2
|
+
//#region src/grep.ts
|
|
3
|
+
/** Run `pattern` on the n-gram sub-index `name` of `index` and collect every hit plus the truncation flag. */
|
|
4
|
+
async function grep(index, name, pattern, options = {}) {
|
|
5
|
+
const it = newNgramAccess(name).get(index).search({
|
|
6
|
+
...options,
|
|
7
|
+
pattern
|
|
8
|
+
});
|
|
9
|
+
const hits = [];
|
|
10
|
+
for (;;) {
|
|
11
|
+
const step = await it.next();
|
|
12
|
+
if (step.done) return {
|
|
13
|
+
hits,
|
|
14
|
+
truncated: step.value?.truncated ?? false
|
|
15
|
+
};
|
|
16
|
+
hits.push(step.value);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { grep };
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=grep.js.map
|
package/dist/grep.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grep.js","names":[],"sources":["../src/grep.ts"],"sourcesContent":["import type { Index } from \"@statewalker/indexer-api\";\nimport { newNgramAccess } from \"./access.js\";\nimport type { NgramQuery, NgramResult } from \"./types.js\";\n\nexport type GrepOptions = Omit<NgramQuery, \"pattern\">;\n\nexport interface GrepResult {\n hits: NgramResult[];\n /** `true` when the backend stopped early (candidate budget or `topK`); `hits` may be incomplete. */\n truncated: boolean;\n}\n\n/** Run `pattern` on the n-gram sub-index `name` of `index` and collect every hit plus the truncation flag. */\nexport async function grep(\n index: Index,\n name: string,\n pattern: string,\n options: GrepOptions = {},\n): Promise<GrepResult> {\n const it = newNgramAccess(name)\n .get(index)\n .search({ ...options, pattern });\n const hits: NgramResult[] = [];\n for (;;) {\n const step = await it.next();\n if (step.done) return { hits, truncated: step.value?.truncated ?? false };\n hits.push(step.value);\n }\n}\n"],"mappings":";;;AAaA,eAAsB,KACpB,OACA,MACA,SACA,UAAuB,CAAC,GACH;CACrB,MAAM,KAAK,eAAe,IAAI,CAAC,CAC5B,IAAI,KAAK,CAAC,CACV,OAAO;EAAE,GAAG;EAAS;CAAQ,CAAC;CACjC,MAAM,OAAsB,CAAC;CAC7B,SAAS;EACP,MAAM,OAAO,MAAM,GAAG,KAAK;EAC3B,IAAI,KAAK,MAAM,OAAO;GAAE;GAAM,WAAW,KAAK,OAAO,aAAa;EAAM;EACxE,KAAK,KAAK,KAAK,KAAK;CACtB;AACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { NGRAM_TYPE, NgramBlock, NgramConfig, NgramIndex, NgramIndexInfo, NgramMatch, NgramQuery, NgramResult, NgramSearchSummary } from "./types.js";
|
|
2
|
+
import { NgramAccess, newNgramAccess } from "./access.js";
|
|
3
|
+
import { getNgramConfig, setNgramConfig } from "./config.js";
|
|
4
|
+
import { GrepOptions, GrepResult, grep } from "./grep.js";
|
|
5
|
+
export { type GrepOptions, type GrepResult, NGRAM_TYPE, type NgramAccess, type NgramBlock, type NgramConfig, type NgramIndex, type NgramIndexInfo, type NgramMatch, type NgramQuery, type NgramResult, type NgramSearchSummary, getNgramConfig, grep, newNgramAccess, setNgramConfig };
|
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { BlockReference, DocumentPath, Metadata, ScoredHit, SearchIndex } from "@statewalker/indexer-api";
|
|
2
|
+
//#region src/types.d.ts
|
|
3
|
+
/** Stable modality-type discriminator for the n-gram (grep) modality. */
|
|
4
|
+
export declare const NGRAM_TYPE: "ngram";
|
|
5
|
+
/** A block of text for the n-gram sub-index. Matches never cross block boundaries. */
|
|
6
|
+
export interface NgramBlock extends BlockReference {
|
|
7
|
+
content: string;
|
|
8
|
+
metadata?: Metadata;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* N-gram sub-query: an exact pattern, literal by default.
|
|
12
|
+
*
|
|
13
|
+
* `regex: true` reads `pattern` as a JavaScript regular expression compiled
|
|
14
|
+
* without the `u` flag (`.` is one UTF-16 code unit; `\u{…}` is not a
|
|
15
|
+
* code-point escape); patterns containing `\p{` / `\P{` are rejected with a
|
|
16
|
+
* `SyntaxError`. `caseSensitive` defaults to `true` (grep semantics). `topK`
|
|
17
|
+
* (default 100) caps the returned blocks; `0` returns no hits. `topK` / `paths`
|
|
18
|
+
* override the top-level request values for this sub-index. A backend may stop
|
|
19
|
+
* early (a candidate budget — 10,000 in the SQLite backend); the search then
|
|
20
|
+
* reports `truncated: true`, as it does when `topK` cut the result short.
|
|
21
|
+
*/
|
|
22
|
+
export interface NgramQuery {
|
|
23
|
+
pattern: string;
|
|
24
|
+
regex?: boolean;
|
|
25
|
+
caseSensitive?: boolean;
|
|
26
|
+
topK?: number;
|
|
27
|
+
paths?: DocumentPath[];
|
|
28
|
+
}
|
|
29
|
+
/** One match: UTF-16 offsets into the block's content, `end` exclusive; `line` is 1-based. */
|
|
30
|
+
export interface NgramMatch {
|
|
31
|
+
start: number;
|
|
32
|
+
end: number;
|
|
33
|
+
line: number;
|
|
34
|
+
}
|
|
35
|
+
/** A block with at least one match. `score` equals `count`. */
|
|
36
|
+
export interface NgramResult extends ScoredHit {
|
|
37
|
+
count: number;
|
|
38
|
+
matches: NgramMatch[];
|
|
39
|
+
}
|
|
40
|
+
/** Returned by `NgramIndex.search` when the generator finishes. */
|
|
41
|
+
export interface NgramSearchSummary {
|
|
42
|
+
/** `true` when the candidate budget or `topK` cut the search short — results may be incomplete. */
|
|
43
|
+
truncated: boolean;
|
|
44
|
+
}
|
|
45
|
+
export interface NgramIndexInfo {
|
|
46
|
+
store: "fts5" | "postings";
|
|
47
|
+
metadata?: Metadata;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* N-gram / regex sub-index. `search` yields hits and *returns* a summary;
|
|
51
|
+
* `for await` drops the summary — use {@link grep} to read it.
|
|
52
|
+
*/
|
|
53
|
+
export interface NgramIndex extends SearchIndex<NgramBlock, NgramQuery, NgramResult> {
|
|
54
|
+
search(params: NgramQuery): AsyncGenerator<NgramResult, NgramSearchSummary | undefined>;
|
|
55
|
+
getIndexInfo(): Promise<NgramIndexInfo>;
|
|
56
|
+
}
|
|
57
|
+
/** Creation config. `store` picks the backend's candidate store; backends choose the default. */
|
|
58
|
+
export interface NgramConfig {
|
|
59
|
+
store?: "fts5" | "postings";
|
|
60
|
+
metadata?: Metadata;
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;qBASa;;iBAGI,mBAAmB;EAClC;EACA,WAAW;;;;;;;;;;;;;;iBAeI;EACf;EACA;EACA;EACA;EACA,QAAQ;;;iBAIO;EACf;EACA;EACA;;;iBAIe,oBAAoB;EACnC;EACA,SAAS;;;iBAIM;;EAEf;;iBAGe;EACf;EACA,WAAW;;;;;;iBAOI,mBAAmB,YAAY,YAAY,YAAY;EACtE,OAAO,QAAQ,aAAa,eAAe,aAAa;EACxD,gBAAgB,QAAQ;;;iBAIT;EACf;EACA,WAAW"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","names":[],"sources":["../src/types.ts"],"sourcesContent":["import type {\n BlockReference,\n DocumentPath,\n Metadata,\n ScoredHit,\n SearchIndex,\n} from \"@statewalker/indexer-api\";\n\n/** Stable modality-type discriminator for the n-gram (grep) modality. */\nexport const NGRAM_TYPE = \"ngram\" as const;\n\n/** A block of text for the n-gram sub-index. Matches never cross block boundaries. */\nexport interface NgramBlock extends BlockReference {\n content: string;\n metadata?: Metadata;\n}\n\n/**\n * N-gram sub-query: an exact pattern, literal by default.\n *\n * `regex: true` reads `pattern` as a JavaScript regular expression compiled\n * without the `u` flag (`.` is one UTF-16 code unit; `\\u{…}` is not a\n * code-point escape); patterns containing `\\p{` / `\\P{` are rejected with a\n * `SyntaxError`. `caseSensitive` defaults to `true` (grep semantics). `topK`\n * (default 100) caps the returned blocks; `0` returns no hits. `topK` / `paths`\n * override the top-level request values for this sub-index. A backend may stop\n * early (a candidate budget — 10,000 in the SQLite backend); the search then\n * reports `truncated: true`, as it does when `topK` cut the result short.\n */\nexport interface NgramQuery {\n pattern: string;\n regex?: boolean;\n caseSensitive?: boolean;\n topK?: number;\n paths?: DocumentPath[];\n}\n\n/** One match: UTF-16 offsets into the block's content, `end` exclusive; `line` is 1-based. */\nexport interface NgramMatch {\n start: number;\n end: number;\n line: number;\n}\n\n/** A block with at least one match. `score` equals `count`. */\nexport interface NgramResult extends ScoredHit {\n count: number;\n matches: NgramMatch[];\n}\n\n/** Returned by `NgramIndex.search` when the generator finishes. */\nexport interface NgramSearchSummary {\n /** `true` when the candidate budget or `topK` cut the search short — results may be incomplete. */\n truncated: boolean;\n}\n\nexport interface NgramIndexInfo {\n store: \"fts5\" | \"postings\";\n metadata?: Metadata;\n}\n\n/**\n * N-gram / regex sub-index. `search` yields hits and *returns* a summary;\n * `for await` drops the summary — use {@link grep} to read it.\n */\nexport interface NgramIndex extends SearchIndex<NgramBlock, NgramQuery, NgramResult> {\n search(params: NgramQuery): AsyncGenerator<NgramResult, NgramSearchSummary | undefined>;\n getIndexInfo(): Promise<NgramIndexInfo>;\n}\n\n/** Creation config. `store` picks the backend's candidate store; backends choose the default. */\nexport interface NgramConfig {\n store?: \"fts5\" | \"postings\";\n metadata?: Metadata;\n}\n"],"mappings":";;AASA,MAAa,aAAa"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/indexer-ngram",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "N-gram / regex (grep) modality contract for @statewalker/indexer-api: types, typed access handle and a grep helper.",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/statewalker/statewalker-search/issues"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/statewalker/statewalker-search/tree/main/packages/indexer-ngram#readme",
|
|
11
|
+
"author": {
|
|
12
|
+
"name": "Mikhail Kotelnikov",
|
|
13
|
+
"email": "mikhail.kotelnikov@gmail.com"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/statewalker/statewalker-search.git",
|
|
19
|
+
"directory": "packages/indexer-ngram"
|
|
20
|
+
},
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"source": "./src/index.ts",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js",
|
|
26
|
+
"default": "./dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"src"
|
|
32
|
+
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@statewalker/indexer-api": "^0.1.3"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"rimraf": "^6.1.3",
|
|
38
|
+
"tsdown": "^0.23.0",
|
|
39
|
+
"typescript": "^7.0.2",
|
|
40
|
+
"vitest": "^5.0.3"
|
|
41
|
+
},
|
|
42
|
+
"sideEffects": false,
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsdown",
|
|
48
|
+
"test": "vitest run --passWithNoTests",
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"clean": "rimraf dist",
|
|
51
|
+
"lint": "biome check --write .",
|
|
52
|
+
"format": "biome format --write ."
|
|
53
|
+
}
|
|
6
54
|
}
|
package/src/access.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CreateIndexParams,
|
|
3
|
+
Index,
|
|
4
|
+
SearchRequest,
|
|
5
|
+
SearchResult,
|
|
6
|
+
} from "@statewalker/indexer-api";
|
|
7
|
+
import { getSubQuery, getSubResult, setSubQuery } from "@statewalker/indexer-api";
|
|
8
|
+
import { getNgramConfig, setNgramConfig } from "./config.js";
|
|
9
|
+
import type { NgramConfig, NgramIndex, NgramQuery, NgramResult } from "./types.js";
|
|
10
|
+
|
|
11
|
+
/** User-facing typed bundle bound to one n-gram sub-index **name**. */
|
|
12
|
+
export interface NgramAccess {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
/** Throws if no sub-index named `name` is registered on `index`. */
|
|
15
|
+
get(index: Index): NgramIndex;
|
|
16
|
+
tryGet(index: Index): NgramIndex | undefined;
|
|
17
|
+
setConfig(params: CreateIndexParams, config: NgramConfig): void;
|
|
18
|
+
getConfig(params: CreateIndexParams): NgramConfig | undefined;
|
|
19
|
+
setQuery(request: SearchRequest, query: NgramQuery): void;
|
|
20
|
+
getQuery(request: SearchRequest): NgramQuery | undefined;
|
|
21
|
+
getResult(entry: SearchResult): NgramResult | undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function newNgramAccess(name: string): NgramAccess {
|
|
25
|
+
return {
|
|
26
|
+
name,
|
|
27
|
+
get(index: Index): NgramIndex {
|
|
28
|
+
const binding = index.getBinding(name);
|
|
29
|
+
if (!binding) {
|
|
30
|
+
throw new Error(`No sub-index named "${name}" is registered on index "${index.name}"`);
|
|
31
|
+
}
|
|
32
|
+
return binding.index as NgramIndex;
|
|
33
|
+
},
|
|
34
|
+
tryGet: (index) => index.getBinding(name)?.index as NgramIndex | undefined,
|
|
35
|
+
setConfig: (params, config) => setNgramConfig(params, name, config),
|
|
36
|
+
getConfig: (params) => getNgramConfig(params, name),
|
|
37
|
+
setQuery: (request, query) => setSubQuery<NgramQuery>(request, name, query),
|
|
38
|
+
getQuery: (request) => getSubQuery<NgramQuery>(request, name),
|
|
39
|
+
getResult: (entry) => getSubResult<NgramResult>(entry, name),
|
|
40
|
+
};
|
|
41
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CreateIndexParams } from "@statewalker/indexer-api";
|
|
2
|
+
import { NGRAM_TYPE, type NgramConfig } from "./types.js";
|
|
3
|
+
|
|
4
|
+
/** Write an n-gram config for the sub-index named `name` into `params.subIndexes`. */
|
|
5
|
+
export function setNgramConfig(params: CreateIndexParams, name: string, config: NgramConfig): void {
|
|
6
|
+
if (!params.subIndexes) params.subIndexes = {};
|
|
7
|
+
params.subIndexes[name] = { type: NGRAM_TYPE, ...config };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Read the n-gram config for the sub-index named `name`, if any. */
|
|
11
|
+
export function getNgramConfig(params: CreateIndexParams, name: string): NgramConfig | undefined {
|
|
12
|
+
const entry = params.subIndexes?.[name];
|
|
13
|
+
if (!entry || entry.type !== NGRAM_TYPE) return undefined;
|
|
14
|
+
const { type: _type, ...rest } = entry;
|
|
15
|
+
return rest as NgramConfig;
|
|
16
|
+
}
|
package/src/grep.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Index } from "@statewalker/indexer-api";
|
|
2
|
+
import { newNgramAccess } from "./access.js";
|
|
3
|
+
import type { NgramQuery, NgramResult } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export type GrepOptions = Omit<NgramQuery, "pattern">;
|
|
6
|
+
|
|
7
|
+
export interface GrepResult {
|
|
8
|
+
hits: NgramResult[];
|
|
9
|
+
/** `true` when the backend stopped early (candidate budget or `topK`); `hits` may be incomplete. */
|
|
10
|
+
truncated: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Run `pattern` on the n-gram sub-index `name` of `index` and collect every hit plus the truncation flag. */
|
|
14
|
+
export async function grep(
|
|
15
|
+
index: Index,
|
|
16
|
+
name: string,
|
|
17
|
+
pattern: string,
|
|
18
|
+
options: GrepOptions = {},
|
|
19
|
+
): Promise<GrepResult> {
|
|
20
|
+
const it = newNgramAccess(name)
|
|
21
|
+
.get(index)
|
|
22
|
+
.search({ ...options, pattern });
|
|
23
|
+
const hits: NgramResult[] = [];
|
|
24
|
+
for (;;) {
|
|
25
|
+
const step = await it.next();
|
|
26
|
+
if (step.done) return { hits, truncated: step.value?.truncated ?? false };
|
|
27
|
+
hits.push(step.value);
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { type NgramAccess, newNgramAccess } from "./access.js";
|
|
2
|
+
export { getNgramConfig, setNgramConfig } from "./config.js";
|
|
3
|
+
export { type GrepOptions, type GrepResult, grep } from "./grep.js";
|
|
4
|
+
export {
|
|
5
|
+
NGRAM_TYPE,
|
|
6
|
+
type NgramBlock,
|
|
7
|
+
type NgramConfig,
|
|
8
|
+
type NgramIndex,
|
|
9
|
+
type NgramIndexInfo,
|
|
10
|
+
type NgramMatch,
|
|
11
|
+
type NgramQuery,
|
|
12
|
+
type NgramResult,
|
|
13
|
+
type NgramSearchSummary,
|
|
14
|
+
} from "./types.js";
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BlockReference,
|
|
3
|
+
DocumentPath,
|
|
4
|
+
Metadata,
|
|
5
|
+
ScoredHit,
|
|
6
|
+
SearchIndex,
|
|
7
|
+
} from "@statewalker/indexer-api";
|
|
8
|
+
|
|
9
|
+
/** Stable modality-type discriminator for the n-gram (grep) modality. */
|
|
10
|
+
export const NGRAM_TYPE = "ngram" as const;
|
|
11
|
+
|
|
12
|
+
/** A block of text for the n-gram sub-index. Matches never cross block boundaries. */
|
|
13
|
+
export interface NgramBlock extends BlockReference {
|
|
14
|
+
content: string;
|
|
15
|
+
metadata?: Metadata;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* N-gram sub-query: an exact pattern, literal by default.
|
|
20
|
+
*
|
|
21
|
+
* `regex: true` reads `pattern` as a JavaScript regular expression compiled
|
|
22
|
+
* without the `u` flag (`.` is one UTF-16 code unit; `\u{…}` is not a
|
|
23
|
+
* code-point escape); patterns containing `\p{` / `\P{` are rejected with a
|
|
24
|
+
* `SyntaxError`. `caseSensitive` defaults to `true` (grep semantics). `topK`
|
|
25
|
+
* (default 100) caps the returned blocks; `0` returns no hits. `topK` / `paths`
|
|
26
|
+
* override the top-level request values for this sub-index. A backend may stop
|
|
27
|
+
* early (a candidate budget — 10,000 in the SQLite backend); the search then
|
|
28
|
+
* reports `truncated: true`, as it does when `topK` cut the result short.
|
|
29
|
+
*/
|
|
30
|
+
export interface NgramQuery {
|
|
31
|
+
pattern: string;
|
|
32
|
+
regex?: boolean;
|
|
33
|
+
caseSensitive?: boolean;
|
|
34
|
+
topK?: number;
|
|
35
|
+
paths?: DocumentPath[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One match: UTF-16 offsets into the block's content, `end` exclusive; `line` is 1-based. */
|
|
39
|
+
export interface NgramMatch {
|
|
40
|
+
start: number;
|
|
41
|
+
end: number;
|
|
42
|
+
line: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A block with at least one match. `score` equals `count`. */
|
|
46
|
+
export interface NgramResult extends ScoredHit {
|
|
47
|
+
count: number;
|
|
48
|
+
matches: NgramMatch[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Returned by `NgramIndex.search` when the generator finishes. */
|
|
52
|
+
export interface NgramSearchSummary {
|
|
53
|
+
/** `true` when the candidate budget or `topK` cut the search short — results may be incomplete. */
|
|
54
|
+
truncated: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface NgramIndexInfo {
|
|
58
|
+
store: "fts5" | "postings";
|
|
59
|
+
metadata?: Metadata;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* N-gram / regex sub-index. `search` yields hits and *returns* a summary;
|
|
64
|
+
* `for await` drops the summary — use {@link grep} to read it.
|
|
65
|
+
*/
|
|
66
|
+
export interface NgramIndex extends SearchIndex<NgramBlock, NgramQuery, NgramResult> {
|
|
67
|
+
search(params: NgramQuery): AsyncGenerator<NgramResult, NgramSearchSummary | undefined>;
|
|
68
|
+
getIndexInfo(): Promise<NgramIndexInfo>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Creation config. `store` picks the backend's candidate store; backends choose the default. */
|
|
72
|
+
export interface NgramConfig {
|
|
73
|
+
store?: "fts5" | "postings";
|
|
74
|
+
metadata?: Metadata;
|
|
75
|
+
}
|