Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {
} from '@kbn/core-saved-objects-api-server';
import type { Logger } from '@kbn/logging';
import type { SmlTypeRegistry } from './sml_type_registry';
import type { SmlIndexAction, SmlContext } from './types';
import type { SmlIndexAction, SmlContext, SmlDocument } from './types';
import { createSmlStorage, smlIndexName } from './sml_storage';
import { isNotFoundError } from './sml_service';

Expand Down Expand Up @@ -124,20 +124,30 @@ class SmlIndexerImpl implements SmlIndexer {
const now = new Date().toISOString();
const bulkOps = smlData.chunks.map((chunk) => {
const chunkId = `${attachmentType}:${originId}:${uuidv4()}`;
const document: SmlDocument = {
id: chunkId,
type: chunk.type,
title: chunk.title,
origin_id: originId,
content: chunk.content,
created_at: now,
updated_at: now,
spaces,
permissions: chunk.permissions ?? [],
};
if (chunk.description !== undefined) {
document.description = chunk.description;
}
if (chunk.user_id !== undefined) {
document.user_id = chunk.user_id;
}
if (chunk.references !== undefined) {
document.references = chunk.references;
}
return {
index: {
_id: chunkId,
document: {
id: chunkId,
type: chunk.type,
title: chunk.title,
origin_id: originId,
content: chunk.content,
created_at: now,
updated_at: now,
spaces,
permissions: chunk.permissions ?? [],
},
document,
},
};
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,19 @@ describe('SmlService', () => {
bool: {
must: [
{
multi_match: {
query: 'foo bar',
type: 'bool_prefix',
fields: saytBoolPrefixFields,
bool: {
should: [
{
multi_match: {
query: 'foo bar',
type: 'bool_prefix',
fields: saytBoolPrefixFields,
},
},
{ match: { content: 'foo bar' } },
{ match: { description: 'foo bar' } },
],
minimum_should_match: 1,
},
},
],
Expand Down Expand Up @@ -247,7 +256,7 @@ describe('SmlService', () => {
});

const call = esClient.search.mock.calls[0]![0]!;
expect(call._source).toEqual({ excludes: ['content'] });
expect(call._source).toEqual({ excludes: ['content', 'description'] });
});

it('uses match_all for query "*"', async () => {
Expand Down Expand Up @@ -312,6 +321,9 @@ describe('SmlService', () => {
title: 'My Viz',
origin_id: 'ref-1',
content: 'content text',
description: 'A lens viz',
user_id: 'user-1',
references: ['lens:other:uuid'],
created_at: '2024-01-01',
updated_at: '2024-01-02',
spaces: ['default'],
Expand All @@ -338,6 +350,9 @@ describe('SmlService', () => {
title: 'My Viz',
origin_id: 'ref-1',
content: 'content text',
description: 'A lens viz',
user_id: 'user-1',
references: ['lens:other:uuid'],
created_at: '2024-01-01',
updated_at: '2024-01-02',
spaces: ['default'],
Expand Down Expand Up @@ -803,6 +818,9 @@ describe('SmlService', () => {
title: 'Doc 2',
origin_id: 'ref-2',
content: 'content 2',
description: 'dash desc',
user_id: 'u2',
references: ['lens:x:y'],
created_at: '2024-01-01',
updated_at: '2024-01-02',
spaces: ['default'],
Expand Down Expand Up @@ -837,6 +855,9 @@ describe('SmlService', () => {
title: 'Doc 2',
origin_id: 'ref-2',
content: 'content 2',
description: 'dash desc',
user_id: 'u2',
references: ['lens:x:y'],
created_at: '2024-01-01',
updated_at: '2024-01-02',
spaces: ['default'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,19 +316,31 @@ const SML_SEARCH_AS_YOU_TYPE_FIELDS = [
] as const;

/**
* Build the search query from a single string. Only `type` and `title` (search_as_you_type + bool_prefix) are searched.
* After trim: empty string or `*` → `match_all` (return everything)
* Build the search query from a single string. `type` and `title` use search_as_you_type + bool_prefix
* for autocomplete-style matching, while `content` and `description` (semantic_text fields) are
* matched with standard `match` queries so longer-form text is also retrievable.
*
* After trim: empty string or `*` → `match_all` (return everything).
*/
const buildSmlSearchQuery = (query: string): Record<string, unknown> => {
const trimmed = query.trim();
if (trimmed === '' || trimmed === '*') {
return { match_all: {} };
}
return {
multi_match: {
query: trimmed,
type: 'bool_prefix',
fields: [...SML_SEARCH_AS_YOU_TYPE_FIELDS],
bool: {
should: [
{
multi_match: {
query: trimmed,
type: 'bool_prefix',
fields: [...SML_SEARCH_AS_YOU_TYPE_FIELDS],
},
},
{ match: { content: trimmed } },
{ match: { description: trimmed } },
],
minimum_should_match: 1,
},
};
};
Expand Down Expand Up @@ -379,7 +391,7 @@ const searchSml = async ({
],
},
},
_source: skipContent ? { excludes: ['content'] } : true,
_source: skipContent ? { excludes: ['content', 'description'] } : true,
});

const total =
Expand All @@ -397,10 +409,13 @@ const searchSml = async ({
title: source.title ?? '',
origin_id: source.origin_id ?? '',
content: source.content,
description: source.description,
references: source.references,
created_at: source.created_at ?? '',
updated_at: source.updated_at ?? '',
spaces: source.spaces ?? [],
permissions: source.permissions ?? [],
user_id: source.user_id,
score: hit._score ?? 0,
};
});
Expand Down Expand Up @@ -470,6 +485,15 @@ const getDocumentsByIds = async ({
spaces: source.spaces ?? [],
permissions: source.permissions ?? [],
};
if (source.description !== undefined) {
doc.description = source.description;
}
if (source.user_id !== undefined) {
doc.user_id = source.user_id;
}
if (source.references !== undefined) {
doc.references = source.references;
}
docMap.set(doc.id, doc);
}
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ const smlStorageSchemaProperties = {
}),
title: types.search_as_you_type({}),
origin_id: types.keyword({}),
content: types.text({}),
content: types.semantic_text({}),
description: types.semantic_text({}),
user_id: types.keyword({}),
references: types.keyword({}),
created_at: types.date({}),
updated_at: types.date({}),
spaces: types.keyword({}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,16 @@ import type { AttachmentInput } from '@kbn/agent-builder-common/attachments';
export interface SmlChunk {
/** Type of the chunk (e.g., 'dashboard', 'lens', 'esql') */
type: string;
/** Searchable content text */
/** Searchable content (indexed as `semantic_text`) */
content: string;
/** Display title */
title: string;
/** Longer summary for semantic search (indexed as `semantic_text`); omit or empty if none */
description?: string;
/** Owner or last-modifier user id when known */
user_id?: string;
/** Other SML chunk ids this item references */
references?: string[];
/** Permissions required to access the underlying element (e.g., 'saved_object:lens/get') */
permissions?: string[];
}
Expand Down Expand Up @@ -116,8 +122,14 @@ export interface SmlDocument {
title: string;
/** Origin ID (e.g., saved object ID) */
origin_id: string;
/** Searchable content */
/** Searchable content (`semantic_text` in the index) */
content: string;
/** Semantic summary (`semantic_text` in the index) */
description?: string;
/** Owner or last-modifier user id */
user_id?: string;
/** Referenced SML chunk ids */
references?: string[];
/** Timestamp when first created */
created_at: string;
/** Timestamp when last updated */
Expand All @@ -130,7 +142,7 @@ export interface SmlDocument {

/**
* An SML search result — same fields as {@link SmlDocument} plus relevance score.
* `content` is optional when the query excluded it from `_source` (e.g. `skipContent`).
* `content` and `description` are optional when excluded from `_source` (e.g. `skipContent`).
*/
export type SmlSearchResult = Omit<SmlDocument, 'content'> & {
content?: string;
Expand Down
Loading