1import { describe, expect, it } from "vitest";
2
3import {
4 contentMapping,
5 DEFAULT_KNN_EF_CONSTRUCTION,
6 DEFAULT_KNN_M,
7 DEFAULT_VECTOR_DIMENSION,
8 TYPE_ENUM,
9 vectorDimensionMatches,
10} from "./mapping.js";
11import { SEARCH_TYPES } from "./types.js";
12
13function props(m: Record<string, unknown>): Record<string, unknown> {
14 return ((m.mappings as { properties: Record<string, unknown> }).properties) as Record<string, unknown>;
15}
16
17describe("contentMapping", () => {
18 it("uses the OpenSearch knn_vector + HNSW shape (2.15 — no dense_vector)", () => {
19 const m = contentMapping();
20 const p = props(m);
21
22
23
24
25
26
27
28
29
30
31
32 expect(p.vector).toEqual(
33 expect.objectContaining({
34 type: "knn_vector",
35 dimension: DEFAULT_VECTOR_DIMENSION,
36 method: expect.objectContaining({
37 name: "hnsw",
38 space_type: "l2",
39 engine: "faiss",
40 }),
41 }),
42 );
43 const method = (p.vector as { method: { parameters: { m: number; ef_construction: number } } }).method;
44 expect(method.parameters.m).toBe(DEFAULT_KNN_M);
45 expect(method.parameters.ef_construction).toBe(DEFAULT_KNN_EF_CONSTRUCTION);
46 });
47
48 it("matches the camelCase SearchDoc field names (the layout decision)", () => {
49 const p = props(contentMapping());
50 for (const field of [
51 "docId",
52 "type",
53 "id",
54 "workspaceId",
55 "teamId",
56 "title",
57 "body",
58 "chunkId",
59 "chunkIndex",
60 "parentDocId",
61 "vectorModel",
62 "vector",
63 "grants",
64 "archived",
65 "pageId",
66 "blockId",
67 "databaseId",
68 "recordId",
69 "createdBy",
70 "createdByName",
71 "icon",
72 "url",
73 "size",
74 "mimeType",
75 "createdAt",
76 "updatedAt",
77 ]) {
78 expect(p, `missing field ${field}`).toHaveProperty(field);
79 }
80
81 for (const field of ["workspace_id", "team_id", "updated_at", "created_by", "parent_doc_id"]) {
82 expect(p, `stale snake_case field ${field}`).not.toHaveProperty(field);
83 }
84 });
85
86 it("backs prefix + fuzzy with title.keyword", () => {
87 const p = props(contentMapping());
88 const title = p.title as { fields: { keyword: { type: string } } };
89 expect(title.fields.keyword.type).toBe("keyword");
90 });
91
92 it("types the 7 public doc types + chunk (TASK-0099)", () => {
93 const p = props(contentMapping());
94
95
96
97
98
99 expect(p.type).toEqual({ type: "keyword" });
100
101
102 expect(TYPE_ENUM).toContain("chunk");
103 expect(SEARCH_TYPES).not.toContain("chunk" as never);
104
105 expect(p.chunkId).toEqual({ type: "keyword" });
106 });
107
108 it("keeps the 1 s refresh interval (the lag SLA)", () => {
109 const m = contentMapping();
110 const settings = (m.settings as { index: { refresh_interval: string; knn: boolean } }).index;
111 expect(settings.refresh_interval).toBe("1s");
112 expect(settings.knn).toBe(true);
113 });
114
115 it("honours the vector tuning options", () => {
116 const p = props(contentMapping({ dimension: 768, m: 32, efConstruction: 128 }));
117 expect((p.vector as { dimension: number }).dimension).toBe(768);
118 const method = (p.vector as { method: { parameters: { m: number; ef_construction: number } } }).method;
119 expect(method.parameters.m).toBe(32);
120 expect(method.parameters.ef_construction).toBe(128);
121 });
122});
123
124describe("vectorDimensionMatches", () => {
125 it("guards the alias-rollover (dims are immutable)", () => {
126 expect(vectorDimensionMatches({ mappings: { properties: { vector: { dimension: 1536 } } } }, 1536)).toBe(true);
127 expect(vectorDimensionMatches({ mappings: { properties: { vector: { dimension: 1024 } } } }, 1536)).toBe(false);
128 expect(vectorDimensionMatches({}, 1536)).toBe(false);
129 });
130});
131