@byline/search-analysis 4.9.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.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import { describe, expect, it } from 'vitest';
9
+ import { encodeSqlToken } from './sql-token-codec.js';
10
+ describe('SQL token codec', () => {
11
+ it.each([
12
+ ['exact', 'a'],
13
+ ['exact', 'ฐานข้อมูล'],
14
+ ['gram', '数据'],
15
+ ['identifier', 'https://example.com/a'],
16
+ ])('encodes a %s token as parser-safe lowercase ASCII', (kind, value) => {
17
+ const encoded = encodeSqlToken({ kind, value });
18
+ expect(encoded).toMatch(/^[a-z0-9]+$/);
19
+ expect(encoded.length).toBeGreaterThan(3);
20
+ });
21
+ it('keeps logical token classes physically distinct', () => {
22
+ const exact = encodeSqlToken({ kind: 'exact', value: 'database' });
23
+ const stem = encodeSqlToken({ kind: 'stem', value: 'database' });
24
+ const gram = encodeSqlToken({ kind: 'gram', value: 'database' });
25
+ expect(new Set([exact, stem, gram]).size).toBe(3);
26
+ });
27
+ it('hashes long terms deterministically within the configured bound', () => {
28
+ const token = { kind: 'identifier', value: `https://example.com/${'x'.repeat(500)}` };
29
+ const first = encodeSqlToken(token, { maxLength: 32 });
30
+ expect(first).toHaveLength(32);
31
+ expect(encodeSqlToken(token, { maxLength: 32 })).toBe(first);
32
+ expect(encodeSqlToken({ ...token, value: `${token.value}y` }, { maxLength: 32 })).not.toBe(first);
33
+ });
34
+ it('rejects empty values and unsafe length limits', () => {
35
+ expect(() => encodeSqlToken({ kind: 'exact', value: '' })).toThrow(TypeError);
36
+ expect(() => encodeSqlToken({ kind: 'exact', value: 'term' }, { maxLength: 15 })).toThrow(RangeError);
37
+ });
38
+ });
@@ -0,0 +1,125 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import type { SearchMatching, SearchPhraseMode, SearchTermOperator } from '@byline/core';
9
+ /** Portable logical token classes. Physical backend representations are separate. */
10
+ export type LogicalTokenKind = 'exact' | 'stem' | 'lemma' | 'normalized' | 'identifier' | 'gram';
11
+ /** Domain category attached to an identifier token for diagnostics and tuning. */
12
+ export type SearchIdentifierKind = 'url' | 'email' | 'mention' | 'hashtag' | 'sku' | 'version' | 'technical';
13
+ /**
14
+ * One logical term produced by portable analysis. All offsets use UTF-16
15
+ * string indexes, matching `Intl.Segmenter` and JavaScript `slice`.
16
+ */
17
+ export interface LogicalToken {
18
+ kind: LogicalTokenKind;
19
+ value: string;
20
+ /** Offset into normalized search text. */
21
+ normalizedStart: number;
22
+ /** Exclusive offset into normalized search text. */
23
+ normalizedEnd: number;
24
+ /** Offset into the original, unmodified input. */
25
+ start: number;
26
+ /** Exclusive offset into the original, unmodified input. */
27
+ end: number;
28
+ /** Source-order position; expansions retain their source token's position. */
29
+ position: number;
30
+ locale: string;
31
+ identifierKind?: SearchIdentifierKind;
32
+ }
33
+ /** Result of analyzing one original text value. */
34
+ export interface AnalyzedText {
35
+ original: string;
36
+ normalized: string;
37
+ locale: string;
38
+ tokens: LogicalToken[];
39
+ exactTokens: LogicalToken[];
40
+ derivedTokens: LogicalToken[];
41
+ identifierTokens: LogicalToken[];
42
+ gramTokens: LogicalToken[];
43
+ analyzerFingerprint: string;
44
+ }
45
+ /** A variant emitted by an optional language-specific token expander. */
46
+ export interface SearchTokenExpansion {
47
+ kind: 'stem' | 'lemma' | 'normalized';
48
+ value: string;
49
+ }
50
+ /**
51
+ * Plug-in point for stemming, lemmatization, or language-specific normalized
52
+ * variants. Expansion augments exact terms; it never replaces them.
53
+ */
54
+ export interface SearchTokenExpander {
55
+ /** Stable family/version component included in the analyzer fingerprint. */
56
+ readonly fingerprint: string;
57
+ supports(locale: string): boolean;
58
+ expand(token: LogicalToken): readonly SearchTokenExpansion[];
59
+ }
60
+ /** One user concept with its recall alternatives kept together. */
61
+ export interface QueryConcept {
62
+ index: number;
63
+ position: number;
64
+ exactTokens: LogicalToken[];
65
+ stemTokens: LogicalToken[];
66
+ lemmaTokens: LogicalToken[];
67
+ normalizedTokens: LogicalToken[];
68
+ identifierTokens: LogicalToken[];
69
+ /**
70
+ * Ordered grams that can act as low-weight fallback for this concept.
71
+ * They are a sequence, not independent OR alternatives.
72
+ */
73
+ gramTokens: LogicalToken[];
74
+ }
75
+ /** Ordered concept indexes that form a phrase constraint or boost. */
76
+ export interface QueryPhrase {
77
+ conceptIndexes: number[];
78
+ explicit: boolean;
79
+ }
80
+ /** Resolved matching policy with analyzer defaults applied. */
81
+ export interface ResolvedSearchMatching {
82
+ operator: SearchTermOperator;
83
+ phrase: SearchPhraseMode;
84
+ minimumShouldMatch?: number;
85
+ }
86
+ /**
87
+ * Backend-neutral lexical query plan. Adapters translate concept alternatives,
88
+ * phrase order, and gram fallback into native query syntax.
89
+ */
90
+ export interface PortableQueryPlan {
91
+ original: string;
92
+ normalized: string;
93
+ locale: string;
94
+ concepts: QueryConcept[];
95
+ phrases: QueryPhrase[];
96
+ /** Ordered fallback grams, including grams that cross word boundaries. */
97
+ gramSequences: LogicalToken[][];
98
+ matching: ResolvedSearchMatching;
99
+ analyzerFingerprint: string;
100
+ }
101
+ export interface AnalyzeTextInput {
102
+ text: string;
103
+ /** Declared field/document locale. Invalid or unsupported values fall back safely. */
104
+ locale?: string;
105
+ }
106
+ export interface AnalyzeQueryInput {
107
+ query: string;
108
+ locale?: string;
109
+ matching?: SearchMatching;
110
+ }
111
+ export interface PortableSearchAnalyzer {
112
+ readonly fingerprint: string;
113
+ analyzeText(input: AnalyzeTextInput): AnalyzedText;
114
+ analyzeQuery(input: AnalyzeQueryInput): PortableQueryPlan;
115
+ }
116
+ export interface PortableSearchAnalyzerOptions {
117
+ /** Fallback after declared locale and script detection. Defaults to `en`. */
118
+ defaultLocale?: string;
119
+ /** Locale chosen for otherwise ambiguous Han-only text. Defaults to `zh`. */
120
+ hanLocale?: 'zh' | 'ja';
121
+ /** Emit overlapping Han bigrams. Defaults to true. */
122
+ hanBigrams?: boolean;
123
+ /** Optional exact-preserving language expansion stages. */
124
+ expanders?: readonly SearchTokenExpander[];
125
+ }
package/dist/types.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ export {};
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@byline/search-analysis",
3
+ "private": false,
4
+ "license": "MPL-2.0",
5
+ "version": "4.9.0",
6
+ "engines": {
7
+ "node": ">=20.9.0"
8
+ },
9
+ "description": "Portable multilingual term analysis, query planning, and highlighting for Byline CMS search providers",
10
+ "keywords": [
11
+ "cms",
12
+ "headless cms",
13
+ "search",
14
+ "multilingual",
15
+ "tokenization"
16
+ ],
17
+ "homepage": "https://github.com/Byline-CMS/bylinecms.dev",
18
+ "bugs": {
19
+ "url": "https://github.com/Byline-CMS/bylinecms.dev/issues"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/Byline-CMS/bylinecms.dev.git",
24
+ "directory": "packages/search-analysis"
25
+ },
26
+ "type": "module",
27
+ "main": "dist/index.js",
28
+ "index": "dist/index.js",
29
+ "types": "dist/index.d.ts",
30
+ "sideEffects": false,
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js",
35
+ "require": "./dist/index.js"
36
+ },
37
+ "./package.json": "./package.json"
38
+ },
39
+ "files": [
40
+ "dist"
41
+ ],
42
+ "dependencies": {
43
+ "@byline/core": "4.9.0"
44
+ },
45
+ "devDependencies": {
46
+ "@biomejs/biome": "2.5.4",
47
+ "@types/node": "^26.1.1",
48
+ "chokidar": "^5.0.0",
49
+ "chokidar-cli": "^3.0.0",
50
+ "npm-run-all": "^4.1.5",
51
+ "tsc-alias": "^1.9.1",
52
+ "typescript": "^7.0.2",
53
+ "vitest": "^4.1.10"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public",
57
+ "registry": "https://registry.npmjs.org/"
58
+ },
59
+ "scripts": {
60
+ "dev": "chokidar 'src/**/*' -c 'npm-run-all build'",
61
+ "build": "tsc -p tsconfig.json && tsc-alias",
62
+ "clean": "node scripts/clean.js node_modules dist build .turbo",
63
+ "lint": "biome check --write --unsafe --diagnostic-level=error",
64
+ "test": "vitest run --mode=node",
65
+ "test:watch": "vitest --mode=node",
66
+ "typecheck": "tsc --noEmit"
67
+ }
68
+ }