@cogenta/schema 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/dist/define-collection.d.ts +8 -0
- package/dist/define-collection.d.ts.map +1 -0
- package/dist/define-collection.js +311 -0
- package/dist/define-collection.js.map +1 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +18 -0
- package/dist/errors.js.map +1 -0
- package/dist/fields.d.ts +117 -0
- package/dist/fields.d.ts.map +1 -0
- package/dist/fields.js +107 -0
- package/dist/fields.js.map +1 -0
- package/dist/generate-schema-json.d.ts +61 -0
- package/dist/generate-schema-json.d.ts.map +1 -0
- package/dist/generate-schema-json.js +128 -0
- package/dist/generate-schema-json.js.map +1 -0
- package/dist/generate-types.d.ts +5 -0
- package/dist/generate-types.d.ts.map +1 -0
- package/dist/generate-types.js +220 -0
- package/dist/generate-types.js.map +1 -0
- package/dist/id.d.ts +8 -0
- package/dist/id.d.ts.map +1 -0
- package/dist/id.js +8 -0
- package/dist/id.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/rich-text.d.ts +164 -0
- package/dist/rich-text.d.ts.map +1 -0
- package/dist/rich-text.js +97 -0
- package/dist/rich-text.js.map +1 -0
- package/dist/routing/ids.d.ts +18 -0
- package/dist/routing/ids.d.ts.map +1 -0
- package/dist/routing/ids.js +41 -0
- package/dist/routing/ids.js.map +1 -0
- package/dist/routing/index.d.ts +13 -0
- package/dist/routing/index.d.ts.map +1 -0
- package/dist/routing/index.js +7 -0
- package/dist/routing/index.js.map +1 -0
- package/dist/routing/redirects.d.ts +76 -0
- package/dist/routing/redirects.d.ts.map +1 -0
- package/dist/routing/redirects.js +237 -0
- package/dist/routing/redirects.js.map +1 -0
- package/dist/routing/resolve.d.ts +38 -0
- package/dist/routing/resolve.d.ts.map +1 -0
- package/dist/routing/resolve.js +23 -0
- package/dist/routing/resolve.js.map +1 -0
- package/dist/routing/router.d.ts +51 -0
- package/dist/routing/router.d.ts.map +1 -0
- package/dist/routing/router.js +132 -0
- package/dist/routing/router.js.map +1 -0
- package/dist/routing/slug-change.d.ts +39 -0
- package/dist/routing/slug-change.d.ts.map +1 -0
- package/dist/routing/slug-change.js +44 -0
- package/dist/routing/slug-change.js.map +1 -0
- package/dist/routing/slug.d.ts +82 -0
- package/dist/routing/slug.d.ts.map +1 -0
- package/dist/routing/slug.js +124 -0
- package/dist/routing/slug.js.map +1 -0
- package/dist/routing/slugify.d.ts +29 -0
- package/dist/routing/slugify.d.ts.map +1 -0
- package/dist/routing/slugify.js +104 -0
- package/dist/routing/slugify.js.map +1 -0
- package/dist/scheduling/index.d.ts +3 -0
- package/dist/scheduling/index.d.ts.map +1 -0
- package/dist/scheduling/index.js +2 -0
- package/dist/scheduling/index.js.map +1 -0
- package/dist/scheduling/publish.d.ts +71 -0
- package/dist/scheduling/publish.d.ts.map +1 -0
- package/dist/scheduling/publish.js +122 -0
- package/dist/scheduling/publish.js.map +1 -0
- package/dist/search/extract.d.ts +25 -0
- package/dist/search/extract.d.ts.map +1 -0
- package/dist/search/extract.js +188 -0
- package/dist/search/extract.js.map +1 -0
- package/dist/search/index.d.ts +36 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +44 -0
- package/dist/search/index.js.map +1 -0
- package/dist/search/mysql.d.ts +7 -0
- package/dist/search/mysql.d.ts.map +1 -0
- package/dist/search/mysql.js +121 -0
- package/dist/search/mysql.js.map +1 -0
- package/dist/search/postgres.d.ts +9 -0
- package/dist/search/postgres.d.ts.map +1 -0
- package/dist/search/postgres.js +167 -0
- package/dist/search/postgres.js.map +1 -0
- package/dist/search/query.d.ts +35 -0
- package/dist/search/query.d.ts.map +1 -0
- package/dist/search/query.js +116 -0
- package/dist/search/query.js.map +1 -0
- package/dist/search/sqlite.d.ts +16 -0
- package/dist/search/sqlite.d.ts.map +1 -0
- package/dist/search/sqlite.js +166 -0
- package/dist/search/sqlite.js.map +1 -0
- package/dist/search/table.d.ts +49 -0
- package/dist/search/table.d.ts.map +1 -0
- package/dist/search/table.js +86 -0
- package/dist/search/table.js.map +1 -0
- package/dist/search/text.d.ts +42 -0
- package/dist/search/text.d.ts.map +1 -0
- package/dist/search/text.js +56 -0
- package/dist/search/text.js.map +1 -0
- package/dist/search/types.d.ts +98 -0
- package/dist/search/types.d.ts.map +1 -0
- package/dist/search/types.js +2 -0
- package/dist/search/types.js.map +1 -0
- package/dist/store/columns.d.ts +33 -0
- package/dist/store/columns.d.ts.map +1 -0
- package/dist/store/columns.js +113 -0
- package/dist/store/columns.js.map +1 -0
- package/dist/store/cursor.d.ts +23 -0
- package/dist/store/cursor.d.ts.map +1 -0
- package/dist/store/cursor.js +36 -0
- package/dist/store/cursor.js.map +1 -0
- package/dist/store/diff.d.ts +59 -0
- package/dist/store/diff.d.ts.map +1 -0
- package/dist/store/diff.js +124 -0
- package/dist/store/diff.js.map +1 -0
- package/dist/store/fragments.d.ts +13 -0
- package/dist/store/fragments.d.ts.map +1 -0
- package/dist/store/fragments.js +25 -0
- package/dist/store/fragments.js.map +1 -0
- package/dist/store/index.d.ts +22 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +16 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/naming.d.ts +25 -0
- package/dist/store/naming.d.ts.map +1 -0
- package/dist/store/naming.js +80 -0
- package/dist/store/naming.js.map +1 -0
- package/dist/store/read-only.d.ts +16 -0
- package/dist/store/read-only.d.ts.map +1 -0
- package/dist/store/read-only.js +36 -0
- package/dist/store/read-only.js.map +1 -0
- package/dist/store/store.d.ts +52 -0
- package/dist/store/store.d.ts.map +1 -0
- package/dist/store/store.js +706 -0
- package/dist/store/store.js.map +1 -0
- package/dist/store/tables.d.ts +40 -0
- package/dist/store/tables.d.ts.map +1 -0
- package/dist/store/tables.js +267 -0
- package/dist/store/tables.js.map +1 -0
- package/dist/store/types.d.ts +105 -0
- package/dist/store/types.d.ts.map +1 -0
- package/dist/store/types.js +2 -0
- package/dist/store/types.js.map +1 -0
- package/dist/store/uuid.d.ts +6 -0
- package/dist/store/uuid.d.ts.map +1 -0
- package/dist/store/uuid.js +64 -0
- package/dist/store/uuid.js.map +1 -0
- package/dist/store/values.d.ts +40 -0
- package/dist/store/values.d.ts.map +1 -0
- package/dist/store/values.js +160 -0
- package/dist/store/values.js.map +1 -0
- package/dist/system-fields.d.ts +60 -0
- package/dist/system-fields.d.ts.map +1 -0
- package/dist/system-fields.js +73 -0
- package/dist/system-fields.js.map +1 -0
- package/dist/types.d.ts +101 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +32 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +54 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +183 -0
- package/dist/validation.js.map +1 -0
- package/package.json +43 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { condense } from './text.js';
|
|
2
|
+
/**
|
|
3
|
+
* Turning an entry into the text that gets indexed.
|
|
4
|
+
*
|
|
5
|
+
* Three sources, and only three (L1 task 16): `text` fields, the spans of a
|
|
6
|
+
* `richText` document, and the data of the blocks. Everything else is either
|
|
7
|
+
* not text — a number, a date, a colour, a media id, a relation id — or is
|
|
8
|
+
* machine data that would pollute the ranking. A `json` field in particular is
|
|
9
|
+
* configuration, not prose: indexing it makes every entry match `true`.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Keys whose values are structure, not content.
|
|
13
|
+
*
|
|
14
|
+
* Without this list a block would contribute `block`, `span`, `normal`,
|
|
15
|
+
* `strong` and a URL to its own relevance, and an entry would rank on how many
|
|
16
|
+
* bold runs it contains.
|
|
17
|
+
*/
|
|
18
|
+
const STRUCTURAL_KEYS = new Set([
|
|
19
|
+
'_key',
|
|
20
|
+
'_type',
|
|
21
|
+
'key',
|
|
22
|
+
'type',
|
|
23
|
+
'id',
|
|
24
|
+
'collection',
|
|
25
|
+
'style',
|
|
26
|
+
'listItem',
|
|
27
|
+
'level',
|
|
28
|
+
'marks',
|
|
29
|
+
'markDefs',
|
|
30
|
+
'href',
|
|
31
|
+
'rel',
|
|
32
|
+
'url',
|
|
33
|
+
'src',
|
|
34
|
+
'mimeType',
|
|
35
|
+
'align',
|
|
36
|
+
'variant',
|
|
37
|
+
'theme',
|
|
38
|
+
'icon',
|
|
39
|
+
]);
|
|
40
|
+
function isRecord(value) {
|
|
41
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Every string in a value, minus the structural keys.
|
|
45
|
+
*
|
|
46
|
+
* Recursive because contract B blocks nest — a `columns` block holds blocks,
|
|
47
|
+
* and a `faq` block holds a list of question/answer pairs. Bounded by depth so
|
|
48
|
+
* that a hand-written or imported block cannot turn indexing into a hang.
|
|
49
|
+
*/
|
|
50
|
+
function collectStrings(value, into, depth = 0) {
|
|
51
|
+
if (depth > 8)
|
|
52
|
+
return;
|
|
53
|
+
if (typeof value === 'string') {
|
|
54
|
+
if (value.length > 0)
|
|
55
|
+
into.push(value);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
for (const item of value)
|
|
60
|
+
collectStrings(item, into, depth + 1);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (!isRecord(value))
|
|
64
|
+
return;
|
|
65
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
66
|
+
if (STRUCTURAL_KEYS.has(key))
|
|
67
|
+
continue;
|
|
68
|
+
collectStrings(nested, into, depth + 1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The prose of a rich text document: the `text` of every span, plus the
|
|
73
|
+
* captions of the media nodes.
|
|
74
|
+
*
|
|
75
|
+
* Deliberately not `JSON.stringify`: the raw document is roughly half syntax,
|
|
76
|
+
* and indexing it would let a search for `span` return every article on the
|
|
77
|
+
* site while a search for a genuinely rare word would be diluted by it.
|
|
78
|
+
*/
|
|
79
|
+
export function extractRichText(value) {
|
|
80
|
+
if (!Array.isArray(value))
|
|
81
|
+
return '';
|
|
82
|
+
const parts = [];
|
|
83
|
+
for (const node of value) {
|
|
84
|
+
if (!isRecord(node))
|
|
85
|
+
continue;
|
|
86
|
+
if (node['_type'] === 'block') {
|
|
87
|
+
const children = node['children'];
|
|
88
|
+
if (!Array.isArray(children))
|
|
89
|
+
continue;
|
|
90
|
+
for (const child of children) {
|
|
91
|
+
if (isRecord(child) && typeof child['text'] === 'string')
|
|
92
|
+
parts.push(child['text']);
|
|
93
|
+
}
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (node['_type'] === 'media') {
|
|
97
|
+
const caption = node['caption'];
|
|
98
|
+
if (typeof caption === 'string')
|
|
99
|
+
parts.push(caption);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
// An unknown node type is still content: a future contract B addition must
|
|
103
|
+
// become searchable on the day it ships, not on the day this file is
|
|
104
|
+
// remembered. The structural keys are filtered out all the same.
|
|
105
|
+
collectStrings(node, parts);
|
|
106
|
+
}
|
|
107
|
+
return condense(parts.join(' '));
|
|
108
|
+
}
|
|
109
|
+
/** The prose of one block. Its `data` is contract B, so its shape is open. */
|
|
110
|
+
export function extractBlockText(block) {
|
|
111
|
+
const parts = [];
|
|
112
|
+
collectStrings(block.data, parts);
|
|
113
|
+
return condense(parts.join(' '));
|
|
114
|
+
}
|
|
115
|
+
function extractZones(zones) {
|
|
116
|
+
const parts = [];
|
|
117
|
+
for (const blocks of Object.values(zones)) {
|
|
118
|
+
for (const block of blocks) {
|
|
119
|
+
const text = extractBlockText(block);
|
|
120
|
+
if (text.length > 0)
|
|
121
|
+
parts.push(text);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return condense(parts.join(' '));
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The field whose value labels a result.
|
|
128
|
+
*
|
|
129
|
+
* `title` by name when the collection has one — contract A's own example uses
|
|
130
|
+
* it — and the first declared `text` field otherwise, which is what an editor
|
|
131
|
+
* sees at the top of the form.
|
|
132
|
+
*/
|
|
133
|
+
function titleOf(collection, entry) {
|
|
134
|
+
const named = collection.fields['title'];
|
|
135
|
+
if (named?.kind === 'text') {
|
|
136
|
+
const value = entry.values['title'];
|
|
137
|
+
if (typeof value === 'string' && value.length > 0)
|
|
138
|
+
return condense(value);
|
|
139
|
+
}
|
|
140
|
+
for (const [name, field] of Object.entries(collection.fields)) {
|
|
141
|
+
if (field.kind !== 'text')
|
|
142
|
+
continue;
|
|
143
|
+
const value = entry.values[name];
|
|
144
|
+
if (typeof value === 'string' && value.length > 0)
|
|
145
|
+
return condense(value);
|
|
146
|
+
}
|
|
147
|
+
return '';
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The indexable document for an entry.
|
|
151
|
+
*
|
|
152
|
+
* The title is repeated inside the body on purpose: none of the three engines
|
|
153
|
+
* offers per-column weighting in a form the other two can match — Postgres has
|
|
154
|
+
* `setweight`, MySQL has nothing, FTS5 has a per-column BM25 weight vector — so
|
|
155
|
+
* the one portable way to make a title term count for more is to let it appear
|
|
156
|
+
* twice in the text every engine ranks.
|
|
157
|
+
*/
|
|
158
|
+
export function searchDocumentFor(collection, entry) {
|
|
159
|
+
const title = titleOf(collection, entry);
|
|
160
|
+
const parts = [];
|
|
161
|
+
if (title.length > 0)
|
|
162
|
+
parts.push(title);
|
|
163
|
+
for (const [name, field] of Object.entries(collection.fields)) {
|
|
164
|
+
const value = entry.values[name];
|
|
165
|
+
if (field.kind === 'text') {
|
|
166
|
+
if (typeof value === 'string' && value.length > 0)
|
|
167
|
+
parts.push(condense(value));
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (field.kind === 'richText') {
|
|
171
|
+
const text = extractRichText(value);
|
|
172
|
+
if (text.length > 0)
|
|
173
|
+
parts.push(text);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const blocks = extractZones(entry.blocks);
|
|
177
|
+
if (blocks.length > 0)
|
|
178
|
+
parts.push(blocks);
|
|
179
|
+
return {
|
|
180
|
+
id: entry.id,
|
|
181
|
+
collection: collection.name,
|
|
182
|
+
locale: entry.locale,
|
|
183
|
+
status: entry.status,
|
|
184
|
+
title,
|
|
185
|
+
body: condense(parts.join(' ')),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=extract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract.js","sourceRoot":"","sources":["../../src/search/extract.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type DatabaseHandle } from '@cogenta/core';
|
|
2
|
+
import type { SearchDriver } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Full-text search, one interface over three engines (L1, task 16).
|
|
5
|
+
*
|
|
6
|
+
* Nothing outside this folder knows whether the site is ranked by `ts_rank`,
|
|
7
|
+
* by an InnoDB fulltext score or by BM25 — or, on a SQLite build without FTS5,
|
|
8
|
+
* not ranked at all. What every caller does know is that the locale and the
|
|
9
|
+
* state it asks for are the only ones it can be given.
|
|
10
|
+
*/
|
|
11
|
+
export { extractBlockText, extractRichText, searchDocumentFor } from './extract.js';
|
|
12
|
+
export type { MysqlSearchOptions } from './mysql.js';
|
|
13
|
+
export { createMysqlSearch } from './mysql.js';
|
|
14
|
+
export type { PostgresSearchOptions } from './postgres.js';
|
|
15
|
+
export { configurationFor, createPostgresSearch } from './postgres.js';
|
|
16
|
+
export type { SqliteSearchOptions } from './sqlite.js';
|
|
17
|
+
export { createSqliteSearch } from './sqlite.js';
|
|
18
|
+
export { SEARCH_COLUMNS, SEARCH_FTS_TABLE, SEARCH_TABLE } from './table.js';
|
|
19
|
+
export { condense, foldText, MIN_TOKEN_LENGTH, queryTokens, tokenize } from './text.js';
|
|
20
|
+
export type { SearchDocument, SearchDriver, SearchHit, SearchQuery, SearchReference, SearchResults, } from './types.js';
|
|
21
|
+
export interface SearchIndexOptions {
|
|
22
|
+
readonly db: DatabaseHandle;
|
|
23
|
+
/** SQLite only: see `SqliteSearchOptions.fts5`. Ignored elsewhere. */
|
|
24
|
+
readonly fts5?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The search index for whichever database is connected.
|
|
28
|
+
*
|
|
29
|
+
* Creating it also creates the physical index — a table plus whatever the
|
|
30
|
+
* engine needs around it — so a fresh install can index its first entry without
|
|
31
|
+
* a migration having run. That is deliberate: search is derived data, it can be
|
|
32
|
+
* rebuilt from the content at any time, and coupling it to the migration
|
|
33
|
+
* history would make a rebuild a schema change.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createSearchIndex(options: SearchIndexOptions): Promise<SearchDriver>;
|
|
36
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/search/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAA;AAIjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C;;;;;;;GAOG;AAEH,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AACnF,YAAY,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAC9C,YAAY,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAC1D,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AACtE,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC3E,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,gBAAgB,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AACvF,YAAY,EACV,cAAc,EACd,YAAY,EACZ,SAAS,EACT,WAAW,EACX,eAAe,EACf,aAAa,GACd,MAAM,YAAY,CAAA;AAEnB,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAA;IAC3B,sEAAsE;IACtE,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CACxB;AAED;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CAe1F"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { CogentaError } from '@cogenta/core';
|
|
2
|
+
import { createMysqlSearch } from './mysql.js';
|
|
3
|
+
import { createPostgresSearch } from './postgres.js';
|
|
4
|
+
import { createSqliteSearch } from './sqlite.js';
|
|
5
|
+
/**
|
|
6
|
+
* Full-text search, one interface over three engines (L1, task 16).
|
|
7
|
+
*
|
|
8
|
+
* Nothing outside this folder knows whether the site is ranked by `ts_rank`,
|
|
9
|
+
* by an InnoDB fulltext score or by BM25 — or, on a SQLite build without FTS5,
|
|
10
|
+
* not ranked at all. What every caller does know is that the locale and the
|
|
11
|
+
* state it asks for are the only ones it can be given.
|
|
12
|
+
*/
|
|
13
|
+
export { extractBlockText, extractRichText, searchDocumentFor } from './extract.js';
|
|
14
|
+
export { createMysqlSearch } from './mysql.js';
|
|
15
|
+
export { configurationFor, createPostgresSearch } from './postgres.js';
|
|
16
|
+
export { createSqliteSearch } from './sqlite.js';
|
|
17
|
+
export { SEARCH_COLUMNS, SEARCH_FTS_TABLE, SEARCH_TABLE } from './table.js';
|
|
18
|
+
export { condense, foldText, MIN_TOKEN_LENGTH, queryTokens, tokenize } from './text.js';
|
|
19
|
+
/**
|
|
20
|
+
* The search index for whichever database is connected.
|
|
21
|
+
*
|
|
22
|
+
* Creating it also creates the physical index — a table plus whatever the
|
|
23
|
+
* engine needs around it — so a fresh install can index its first entry without
|
|
24
|
+
* a migration having run. That is deliberate: search is derived data, it can be
|
|
25
|
+
* rebuilt from the content at any time, and coupling it to the migration
|
|
26
|
+
* history would make a rebuild a schema change.
|
|
27
|
+
*/
|
|
28
|
+
export async function createSearchIndex(options) {
|
|
29
|
+
const { db } = options;
|
|
30
|
+
if (db.dialect === 'postgres')
|
|
31
|
+
return createPostgresSearch({ db });
|
|
32
|
+
if (db.dialect === 'mysql')
|
|
33
|
+
return createMysqlSearch({ db });
|
|
34
|
+
if (db.dialect === 'sqlite') {
|
|
35
|
+
return createSqliteSearch(options.fts5 === undefined ? { db } : { db, fts5: options.fts5 });
|
|
36
|
+
}
|
|
37
|
+
throw new CogentaError({
|
|
38
|
+
code: 'DB_DIALECT_UNSUPPORTED',
|
|
39
|
+
message: `No full-text search implementation for the "${String(db.dialect)}" dialect.`,
|
|
40
|
+
hint: 'Cogenta indexes content on Postgres, MySQL/MariaDB and SQLite.',
|
|
41
|
+
details: { dialect: db.dialect },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/search/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,eAAe,CAAA;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAGhD;;;;;;;GAOG;AAEH,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAEnF,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAE9C,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AAEtE,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC3E,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,gBAAgB,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAgBvF;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,OAA2B;IACjE,MAAM,EAAE,EAAE,EAAE,GAAG,OAAO,CAAA;IAEtB,IAAI,EAAE,CAAC,OAAO,KAAK,UAAU;QAAE,OAAO,oBAAoB,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;IAClE,IAAI,EAAE,CAAC,OAAO,KAAK,OAAO;QAAE,OAAO,iBAAiB,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;IAC5D,IAAI,EAAE,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;QAC5B,OAAO,kBAAkB,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAA;IAC7F,CAAC;IAED,MAAM,IAAI,YAAY,CAAC;QACrB,IAAI,EAAE,wBAAwB;QAC9B,OAAO,EAAE,+CAA+C,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,YAAY;QACtF,IAAI,EAAE,gEAAgE;QACtE,OAAO,EAAE,EAAE,OAAO,EAAE,EAAE,CAAC,OAAO,EAAE;KACjC,CAAC,CAAA;AACJ,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type DatabaseHandle } from '@cogenta/core';
|
|
2
|
+
import type { SearchDriver } from './types.js';
|
|
3
|
+
export interface MysqlSearchOptions {
|
|
4
|
+
readonly db: DatabaseHandle;
|
|
5
|
+
}
|
|
6
|
+
export declare function createMysqlSearch(options: MysqlSearchOptions): Promise<SearchDriver>;
|
|
7
|
+
//# sourceMappingURL=mysql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mysql.d.ts","sourceRoot":"","sources":["../../src/search/mysql.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,cAAc,EAMpB,MAAM,eAAe,CAAA;AAqBtB,OAAO,KAAK,EAAkB,YAAY,EAAgC,MAAM,YAAY,CAAA;AAiC5F,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAA;CAC5B;AAED,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CA2G1F"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { identifier, sql, limit as sqlLimit, } from '@cogenta/core';
|
|
2
|
+
import { joinFragments } from '../store/fragments.js';
|
|
3
|
+
import { assertDocument, assertReference, EMPTY_RESULTS, normaliseQuery, numeric, storedTitle, toResults, } from './query.js';
|
|
4
|
+
import { allOf, baseColumns, primaryKey, SEARCH_COLUMNS, SEARCH_TABLE, scopeFilters, selectedColumns, } from './table.js';
|
|
5
|
+
import { foldText, MIN_TOKEN_LENGTH } from './text.js';
|
|
6
|
+
/**
|
|
7
|
+
* MySQL and MariaDB full-text search: a `FULLTEXT` index, read in boolean mode.
|
|
8
|
+
*
|
|
9
|
+
* Boolean mode rather than natural language mode, for two reasons that both
|
|
10
|
+
* bite in production. Natural language mode on MyISAM drops any word present in
|
|
11
|
+
* more than half the rows — on a site where every page says the brand name,
|
|
12
|
+
* searching for the brand returns nothing, with no error. And natural language
|
|
13
|
+
* mode has no prefix operator, so a half-typed word matches nothing. Boolean
|
|
14
|
+
* mode gives `+term*`: every term required, each one allowed to be a prefix,
|
|
15
|
+
* which is the same semantics this package gives on the other two dialects.
|
|
16
|
+
*
|
|
17
|
+
* What it does not give is a comparable score. `MATCH … AGAINST` in boolean
|
|
18
|
+
* mode returns a term-count-based number, not the IDF-weighted rank Postgres
|
|
19
|
+
* computes. The L1 spec accepts that explicitly, so the contract suite asserts
|
|
20
|
+
* which documents come back and never in which order they are ranked.
|
|
21
|
+
*/
|
|
22
|
+
const FULLTEXT_INDEX = 'cogenta_search_ft';
|
|
23
|
+
const SCOPE_INDEX = 'cogenta_search_scope';
|
|
24
|
+
/**
|
|
25
|
+
* `+term*` for every term.
|
|
26
|
+
*
|
|
27
|
+
* Tokens are letters and digits only (see `text.ts`), so a reader typing
|
|
28
|
+
* `-foo +bar*` gets two ordinary words rather than boolean operators, and the
|
|
29
|
+
* index cannot be steered by the query string.
|
|
30
|
+
*/
|
|
31
|
+
function booleanExpression(tokens) {
|
|
32
|
+
return tokens.map((token) => `+${token}*`).join(' ');
|
|
33
|
+
}
|
|
34
|
+
export async function createMysqlSearch(options) {
|
|
35
|
+
const { db } = options;
|
|
36
|
+
const dialect = 'mysql';
|
|
37
|
+
const table = identifier(SEARCH_TABLE, dialect);
|
|
38
|
+
const content = identifier(SEARCH_COLUMNS.content, dialect);
|
|
39
|
+
// The FULLTEXT key is declared inside `create table`: MySQL has no
|
|
40
|
+
// `create index if not exists`, so adding it afterwards means either a
|
|
41
|
+
// swallowed error or a catalogue lookup, and neither is worth a second
|
|
42
|
+
// statement when the table is created here anyway.
|
|
43
|
+
const columns = [
|
|
44
|
+
...baseColumns(dialect),
|
|
45
|
+
primaryKey(dialect),
|
|
46
|
+
sql `fulltext key ${identifier(FULLTEXT_INDEX, dialect)} (${content})`,
|
|
47
|
+
sql `key ${identifier(SCOPE_INDEX, dialect)} (${identifier(SEARCH_COLUMNS.locale, dialect)},
|
|
48
|
+
${identifier(SEARCH_COLUMNS.status, dialect)})`,
|
|
49
|
+
];
|
|
50
|
+
await db.query(sql `create table if not exists ${table} (${joinFragments(columns, ', ')})`);
|
|
51
|
+
const assignments = [
|
|
52
|
+
SEARCH_COLUMNS.locale,
|
|
53
|
+
SEARCH_COLUMNS.status,
|
|
54
|
+
SEARCH_COLUMNS.title,
|
|
55
|
+
SEARCH_COLUMNS.content,
|
|
56
|
+
].map((column) => {
|
|
57
|
+
const name = identifier(column, dialect);
|
|
58
|
+
// `values(col)` rather than the newer row alias: MariaDB does not accept
|
|
59
|
+
// `as new` at all, and this package is tested against both.
|
|
60
|
+
return sql `${name} = values(${name})`;
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
dialect,
|
|
64
|
+
index: async (entry) => {
|
|
65
|
+
assertDocument(entry);
|
|
66
|
+
await db.query(sql `insert into ${table} (
|
|
67
|
+
${identifier(SEARCH_COLUMNS.entryId, dialect)},
|
|
68
|
+
${identifier(SEARCH_COLUMNS.collection, dialect)},
|
|
69
|
+
${identifier(SEARCH_COLUMNS.locale, dialect)},
|
|
70
|
+
${identifier(SEARCH_COLUMNS.status, dialect)},
|
|
71
|
+
${identifier(SEARCH_COLUMNS.title, dialect)},
|
|
72
|
+
${content})
|
|
73
|
+
values (${entry.id}, ${entry.collection}, ${entry.locale}, ${entry.status},
|
|
74
|
+
${storedTitle(entry)}, ${foldText(`${entry.title} ${entry.body}`)})
|
|
75
|
+
on duplicate key update ${joinFragments(assignments, ', ')}`);
|
|
76
|
+
},
|
|
77
|
+
remove: async (reference) => {
|
|
78
|
+
assertReference(reference);
|
|
79
|
+
await db.query(sql `delete from ${table}
|
|
80
|
+
where ${identifier(SEARCH_COLUMNS.collection, dialect)} = ${reference.collection}
|
|
81
|
+
and ${identifier(SEARCH_COLUMNS.entryId, dialect)} = ${reference.id}`);
|
|
82
|
+
},
|
|
83
|
+
search: async (query) => {
|
|
84
|
+
const normalised = normaliseQuery(query);
|
|
85
|
+
if (normalised.tokens.length === 0)
|
|
86
|
+
return EMPTY_RESULTS;
|
|
87
|
+
const expression = booleanExpression(normalised.tokens);
|
|
88
|
+
const match = sql `match(${content}) against (${expression} in boolean mode)`;
|
|
89
|
+
const filters = scopeFilters(dialect, normalised);
|
|
90
|
+
const found = await db.query(sql `select ${selectedColumns(dialect)}, ${match} as ${identifier('score', dialect)}
|
|
91
|
+
from ${table}
|
|
92
|
+
where ${allOf([match, ...filters])}
|
|
93
|
+
order by ${identifier('score', dialect)} desc,
|
|
94
|
+
${identifier(SEARCH_COLUMNS.entryId, dialect)} asc
|
|
95
|
+
limit ${sqlLimit(normalised.size + 1)} offset ${sqlLimit(normalised.offset)}`);
|
|
96
|
+
return toResults(found.rows, (row) => numeric(row['score']), normalised);
|
|
97
|
+
},
|
|
98
|
+
clear: async (scope) => {
|
|
99
|
+
const where = scope?.collection === undefined
|
|
100
|
+
? sql ``
|
|
101
|
+
: sql ` where ${identifier(SEARCH_COLUMNS.collection, dialect)} = ${scope.collection}`;
|
|
102
|
+
await db.query(sql `delete from ${table}${where}`);
|
|
103
|
+
},
|
|
104
|
+
health: async () => {
|
|
105
|
+
const started = Date.now();
|
|
106
|
+
const counted = await db.query(sql `select count(*) as ${identifier('total', dialect)} from ${table}`);
|
|
107
|
+
return {
|
|
108
|
+
status: 'ok',
|
|
109
|
+
driver: 'mysql',
|
|
110
|
+
tier: 'optimal',
|
|
111
|
+
latencyMs: Date.now() - started,
|
|
112
|
+
// Stated rather than hidden: the server, not Cogenta, decides the floor,
|
|
113
|
+
// and an administrator who wonders why a two-letter word finds nothing
|
|
114
|
+
// needs to be pointed at `innodb_ft_min_token_size`.
|
|
115
|
+
message: `FULLTEXT search in boolean mode. Words shorter than ${MIN_TOKEN_LENGTH} characters, and the server's stopwords, are not indexed.`,
|
|
116
|
+
details: { documents: numeric(counted.rows[0]?.total) },
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=mysql.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mysql.js","sourceRoot":"","sources":["../../src/search/mysql.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type DatabaseHandle } from '@cogenta/core';
|
|
2
|
+
import type { SearchDriver } from './types.js';
|
|
3
|
+
/** `fr-CA`, `fr_CA` and `FR` all mean the French dictionary. */
|
|
4
|
+
export declare function configurationFor(locale: string): string;
|
|
5
|
+
export interface PostgresSearchOptions {
|
|
6
|
+
readonly db: DatabaseHandle;
|
|
7
|
+
}
|
|
8
|
+
export declare function createPostgresSearch(options: PostgresSearchOptions): Promise<SearchDriver>;
|
|
9
|
+
//# sourceMappingURL=postgres.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../src/search/postgres.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,cAAc,EAMpB,MAAM,eAAe,CAAA;AAqBtB,OAAO,KAAK,EAAkB,YAAY,EAAgC,MAAM,YAAY,CAAA;AAwD5F,gEAAgE;AAChE,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGvD;AAeD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAA;CAC5B;AAED,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,YAAY,CAAC,CAqHhG"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { identifier, sql, limit as sqlLimit, } from '@cogenta/core';
|
|
2
|
+
import { joinFragments } from '../store/fragments.js';
|
|
3
|
+
import { assertDocument, assertReference, EMPTY_RESULTS, normaliseQuery, numeric, storedTitle, toResults, } from './query.js';
|
|
4
|
+
import { allOf, baseColumns, primaryKey, SEARCH_COLUMNS, SEARCH_TABLE, scopeFilters, selectedColumns, } from './table.js';
|
|
5
|
+
import { foldText } from './text.js';
|
|
6
|
+
/**
|
|
7
|
+
* Postgres full-text search: a stored `tsvector` and a GIN index.
|
|
8
|
+
*
|
|
9
|
+
* The vector is a real column rather than an expression index, because the
|
|
10
|
+
* text search configuration is **per row**: an entry in French must be stemmed
|
|
11
|
+
* by the French dictionary and an entry in English by the English one, and an
|
|
12
|
+
* expression index can only name one configuration. Storing the vector costs
|
|
13
|
+
* space and buys the right stemmer per language, which is the whole reason to
|
|
14
|
+
* be on Postgres for this feature.
|
|
15
|
+
*/
|
|
16
|
+
const DOCUMENT_COLUMN = 'document';
|
|
17
|
+
const GIN_INDEX = 'cogenta_search_document_gin';
|
|
18
|
+
const SCOPE_INDEX = 'cogenta_search_scope';
|
|
19
|
+
/**
|
|
20
|
+
* Locale to text search configuration.
|
|
21
|
+
*
|
|
22
|
+
* Only configurations that ship with a default Postgres build are listed: a
|
|
23
|
+
* missing one is not a warning but a `text search configuration "…" does not
|
|
24
|
+
* exist` at query time. Anything unlisted falls back to `simple`, which does no
|
|
25
|
+
* stemming and no stopword removal — worse recall, never an error, which is the
|
|
26
|
+
* right trade for a language the server was not built to handle.
|
|
27
|
+
*/
|
|
28
|
+
const CONFIGURATIONS = {
|
|
29
|
+
ar: 'arabic',
|
|
30
|
+
da: 'danish',
|
|
31
|
+
de: 'german',
|
|
32
|
+
el: 'greek',
|
|
33
|
+
en: 'english',
|
|
34
|
+
es: 'spanish',
|
|
35
|
+
eu: 'basque',
|
|
36
|
+
fi: 'finnish',
|
|
37
|
+
fr: 'french',
|
|
38
|
+
ga: 'irish',
|
|
39
|
+
hi: 'hindi',
|
|
40
|
+
hu: 'hungarian',
|
|
41
|
+
hy: 'armenian',
|
|
42
|
+
id: 'indonesian',
|
|
43
|
+
it: 'italian',
|
|
44
|
+
lt: 'lithuanian',
|
|
45
|
+
ne: 'nepali',
|
|
46
|
+
nl: 'dutch',
|
|
47
|
+
no: 'norwegian',
|
|
48
|
+
pt: 'portuguese',
|
|
49
|
+
ro: 'romanian',
|
|
50
|
+
ru: 'russian',
|
|
51
|
+
sr: 'serbian',
|
|
52
|
+
sv: 'swedish',
|
|
53
|
+
ta: 'tamil',
|
|
54
|
+
tr: 'turkish',
|
|
55
|
+
yi: 'yiddish',
|
|
56
|
+
};
|
|
57
|
+
/** `fr-CA`, `fr_CA` and `FR` all mean the French dictionary. */
|
|
58
|
+
export function configurationFor(locale) {
|
|
59
|
+
const primary = locale.toLowerCase().split(/[-_]/u)[0] ?? '';
|
|
60
|
+
return CONFIGURATIONS[primary] ?? 'simple';
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The `tsquery` text: every term required, every term a prefix.
|
|
64
|
+
*
|
|
65
|
+
* Requiring all terms (`&`) rather than any of them matches what a reader
|
|
66
|
+
* expects of a site search — more terms should narrow, not widen. The `:*`
|
|
67
|
+
* makes a half-typed last word still match, which is what a search-as-you-type
|
|
68
|
+
* box needs. Tokens are letters and digits only (see `text.ts`), so nothing in
|
|
69
|
+
* here can be read as `tsquery` syntax.
|
|
70
|
+
*/
|
|
71
|
+
function tsquery(tokens) {
|
|
72
|
+
return tokens.map((token) => `${token}:*`).join(' & ');
|
|
73
|
+
}
|
|
74
|
+
export async function createPostgresSearch(options) {
|
|
75
|
+
const { db } = options;
|
|
76
|
+
const dialect = 'postgres';
|
|
77
|
+
const table = identifier(SEARCH_TABLE, dialect);
|
|
78
|
+
const document = identifier(DOCUMENT_COLUMN, dialect);
|
|
79
|
+
const columns = [
|
|
80
|
+
...baseColumns(dialect),
|
|
81
|
+
sql `${document} tsvector not null`,
|
|
82
|
+
primaryKey(dialect),
|
|
83
|
+
];
|
|
84
|
+
await db.query(sql `create table if not exists ${table} (${joinFragments(columns, ', ')})`);
|
|
85
|
+
// GIN rather than GiST: slower to update, far faster to search, and a content
|
|
86
|
+
// index is read orders of magnitude more often than it is written.
|
|
87
|
+
await db.query(sql `create index if not exists ${identifier(GIN_INDEX, dialect)}
|
|
88
|
+
on ${table} using gin (${document})`);
|
|
89
|
+
await db.query(sql `create index if not exists ${identifier(SCOPE_INDEX, dialect)}
|
|
90
|
+
on ${table} (${identifier(SEARCH_COLUMNS.locale, dialect)},
|
|
91
|
+
${identifier(SEARCH_COLUMNS.status, dialect)})`);
|
|
92
|
+
return {
|
|
93
|
+
dialect,
|
|
94
|
+
index: async (entry) => {
|
|
95
|
+
assertDocument(entry);
|
|
96
|
+
const configuration = configurationFor(entry.locale);
|
|
97
|
+
const content = foldText(`${entry.title} ${entry.body}`);
|
|
98
|
+
// `on conflict` rather than delete-then-insert: re-indexing an entry after
|
|
99
|
+
// an edit is the common path, and two statements would leave the entry
|
|
100
|
+
// missing from the index for the instant between them.
|
|
101
|
+
await db.query(sql `insert into ${table} (
|
|
102
|
+
${identifier(SEARCH_COLUMNS.entryId, dialect)},
|
|
103
|
+
${identifier(SEARCH_COLUMNS.collection, dialect)},
|
|
104
|
+
${identifier(SEARCH_COLUMNS.locale, dialect)},
|
|
105
|
+
${identifier(SEARCH_COLUMNS.status, dialect)},
|
|
106
|
+
${identifier(SEARCH_COLUMNS.title, dialect)},
|
|
107
|
+
${identifier(SEARCH_COLUMNS.content, dialect)},
|
|
108
|
+
${document})
|
|
109
|
+
values (${entry.id}, ${entry.collection}, ${entry.locale}, ${entry.status},
|
|
110
|
+
${storedTitle(entry)}, ${content},
|
|
111
|
+
to_tsvector(${configuration}::regconfig, ${content}))
|
|
112
|
+
on conflict (${identifier(SEARCH_COLUMNS.collection, dialect)},
|
|
113
|
+
${identifier(SEARCH_COLUMNS.entryId, dialect)})
|
|
114
|
+
do update set
|
|
115
|
+
${identifier(SEARCH_COLUMNS.locale, dialect)} = excluded.${identifier(SEARCH_COLUMNS.locale, dialect)},
|
|
116
|
+
${identifier(SEARCH_COLUMNS.status, dialect)} = excluded.${identifier(SEARCH_COLUMNS.status, dialect)},
|
|
117
|
+
${identifier(SEARCH_COLUMNS.title, dialect)} = excluded.${identifier(SEARCH_COLUMNS.title, dialect)},
|
|
118
|
+
${identifier(SEARCH_COLUMNS.content, dialect)} = excluded.${identifier(SEARCH_COLUMNS.content, dialect)},
|
|
119
|
+
${document} = excluded.${document}`);
|
|
120
|
+
},
|
|
121
|
+
remove: async (reference) => {
|
|
122
|
+
assertReference(reference);
|
|
123
|
+
await db.query(sql `delete from ${table}
|
|
124
|
+
where ${identifier(SEARCH_COLUMNS.collection, dialect)} = ${reference.collection}
|
|
125
|
+
and ${identifier(SEARCH_COLUMNS.entryId, dialect)} = ${reference.id}`);
|
|
126
|
+
},
|
|
127
|
+
search: async (query) => {
|
|
128
|
+
const normalised = normaliseQuery(query);
|
|
129
|
+
if (normalised.tokens.length === 0)
|
|
130
|
+
return EMPTY_RESULTS;
|
|
131
|
+
const configuration = configurationFor(normalised.locale);
|
|
132
|
+
const expression = tsquery(normalised.tokens);
|
|
133
|
+
const filters = scopeFilters(dialect, normalised);
|
|
134
|
+
// The query is built twice — once to match, once to rank. Postgres
|
|
135
|
+
// evaluates `to_tsquery` as an immutable function, so this is one parse
|
|
136
|
+
// and no extra work, and it keeps the statement readable.
|
|
137
|
+
const match = sql `${document} @@ to_tsquery(${configuration}::regconfig, ${expression})`;
|
|
138
|
+
const rank = sql `ts_rank(${document}, to_tsquery(${configuration}::regconfig, ${expression}))`;
|
|
139
|
+
const found = await db.query(sql `select ${selectedColumns(dialect)}, ${rank} as ${identifier('score', dialect)}
|
|
140
|
+
from ${table}
|
|
141
|
+
where ${allOf([match, ...filters])}
|
|
142
|
+
order by ${identifier('score', dialect)} desc,
|
|
143
|
+
${identifier(SEARCH_COLUMNS.entryId, dialect)} asc
|
|
144
|
+
limit ${sqlLimit(normalised.size + 1)} offset ${sqlLimit(normalised.offset)}`);
|
|
145
|
+
return toResults(found.rows, (row) => numeric(row['score']), normalised);
|
|
146
|
+
},
|
|
147
|
+
clear: async (scope) => {
|
|
148
|
+
const where = scope?.collection === undefined
|
|
149
|
+
? sql ``
|
|
150
|
+
: sql ` where ${identifier(SEARCH_COLUMNS.collection, dialect)} = ${scope.collection}`;
|
|
151
|
+
await db.query(sql `delete from ${table}${where}`);
|
|
152
|
+
},
|
|
153
|
+
health: async () => {
|
|
154
|
+
const started = Date.now();
|
|
155
|
+
const counted = await db.query(sql `select count(*) as ${identifier('total', dialect)} from ${table}`);
|
|
156
|
+
return {
|
|
157
|
+
status: 'ok',
|
|
158
|
+
driver: 'postgres',
|
|
159
|
+
tier: 'optimal',
|
|
160
|
+
latencyMs: Date.now() - started,
|
|
161
|
+
message: 'Full-text search on tsvector with a GIN index, stemmed per locale.',
|
|
162
|
+
details: { documents: numeric(counted.rows[0]?.total) },
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=postgres.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../src/search/postgres.ts"],"names":[],"mappings":"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"}
|