mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-02 02:00:49 +08:00
feat: normalize search text (#14449)
#### PR Dependency Tree * **PR #14449** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Improvements** * Search text normalization now applied consistently across doc titles, search results, and highlights for uniform display formatting. * **Tests** * Added comprehensive test coverage for search text normalization utility. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -6,6 +6,7 @@ import { isEmpty, omit } from 'lodash-es';
|
||||
import { map, type Observable, of, switchMap } from 'rxjs';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { normalizeSearchText } from '../../../utils/normalize-search-text';
|
||||
import type { DocsService } from '../../doc/services/docs';
|
||||
import type { WorkspaceService } from '../../workspace';
|
||||
|
||||
@@ -26,24 +27,6 @@ export class DocsSearchService extends Service {
|
||||
errorMessage: null,
|
||||
} as IndexerSyncState);
|
||||
|
||||
private normalizeHighlight(value?: string | null) {
|
||||
if (!value) {
|
||||
return value ?? '';
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed.join(' ');
|
||||
}
|
||||
if (typeof parsed === 'string') {
|
||||
return parsed;
|
||||
}
|
||||
} catch {
|
||||
// ignore parse errors, return raw value
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
searchTitle$(query: string) {
|
||||
return this.indexer
|
||||
.search$(
|
||||
@@ -144,12 +127,12 @@ export class DocsSearchService extends Service {
|
||||
const firstMatchFlavour = bucket.hits.nodes[0]?.fields.flavour;
|
||||
if (firstMatchFlavour === 'affine:page') {
|
||||
// is title match
|
||||
const blockContent = this.normalizeHighlight(
|
||||
const blockContent = normalizeSearchText(
|
||||
bucket.hits.nodes[1]?.highlights.content[0]
|
||||
); // try to get block content
|
||||
result.push({
|
||||
docId: bucket.key,
|
||||
title: this.normalizeHighlight(
|
||||
title: normalizeSearchText(
|
||||
bucket.hits.nodes[0].highlights.content[0]
|
||||
),
|
||||
score: bucket.score,
|
||||
@@ -169,7 +152,7 @@ export class DocsSearchService extends Service {
|
||||
? matchedBlockId
|
||||
: matchedBlockId[0],
|
||||
score: bucket.score,
|
||||
blockContent: this.normalizeHighlight(
|
||||
blockContent: normalizeSearchText(
|
||||
bucket.hits.nodes[0]?.highlights.content[0]
|
||||
),
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import { html } from 'lit';
|
||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
||||
import { catchError, map, of } from 'rxjs';
|
||||
|
||||
import { normalizeSearchText } from '../../../utils/normalize-search-text';
|
||||
import type { CollectionMeta, CollectionService } from '../../collection';
|
||||
import type { DocDisplayMetaService } from '../../doc-display-meta';
|
||||
import type { DocsSearchService } from '../../docs-search';
|
||||
@@ -185,14 +186,16 @@ export class SearchMenuService extends Service {
|
||||
typeof node.fields.docId === 'string'
|
||||
? node.fields.docId
|
||||
: node.fields.docId[0];
|
||||
const title =
|
||||
const title = normalizeSearchText(
|
||||
typeof node.fields.title === 'string'
|
||||
? node.fields.title
|
||||
: node.fields.title[0];
|
||||
: node.fields.title[0]
|
||||
);
|
||||
const highlights = normalizeSearchText(node.highlights?.title?.[0]);
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
highlights: node.highlights?.title?.[0],
|
||||
highlights: highlights || undefined,
|
||||
};
|
||||
})
|
||||
)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import { normalizeSearchText } from '../normalize-search-text';
|
||||
|
||||
test('normalizeSearchText should keep plain text unchanged', () => {
|
||||
expect(normalizeSearchText('hello world')).toBe('hello world');
|
||||
});
|
||||
|
||||
test('normalizeSearchText should decode serialized single-item array', () => {
|
||||
expect(normalizeSearchText('["hello world"]')).toBe('hello world');
|
||||
});
|
||||
|
||||
test('normalizeSearchText should decode serialized highlighted array', () => {
|
||||
expect(normalizeSearchText('["<b>hello</b> world"]')).toBe(
|
||||
'<b>hello</b> world'
|
||||
);
|
||||
});
|
||||
|
||||
test('normalizeSearchText should join serialized multi-item array', () => {
|
||||
expect(normalizeSearchText('["hello","world"]')).toBe('hello world');
|
||||
});
|
||||
|
||||
test('normalizeSearchText should decode serialized string', () => {
|
||||
expect(normalizeSearchText('"hello world"')).toBe('hello world');
|
||||
});
|
||||
|
||||
test('normalizeSearchText should keep invalid serialized text unchanged', () => {
|
||||
expect(normalizeSearchText('["hello"')).toBe('["hello"');
|
||||
});
|
||||
|
||||
test('normalizeSearchText should support array input', () => {
|
||||
expect(normalizeSearchText(['hello', 'world'])).toBe('hello world');
|
||||
});
|
||||
@@ -2,6 +2,7 @@ export * from './channel';
|
||||
export * from './create-emotion-cache';
|
||||
export * from './event';
|
||||
export * from './extract-emoji-icon';
|
||||
export * from './normalize-search-text';
|
||||
export * from './string2color';
|
||||
export * from './toast';
|
||||
export * from './unflatten-object';
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
interface NormalizeSearchTextOptions {
|
||||
fallback?: string;
|
||||
arrayJoiner?: string;
|
||||
}
|
||||
|
||||
function tryParseSerializedText(value: string): unknown | null {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Indexer/storage may serialize text arrays as JSON strings like ["foo"].
|
||||
if (!trimmed.startsWith('[') && !trimmed.startsWith('"')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(trimmed);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeSearchText(
|
||||
value: unknown,
|
||||
{ fallback = '', arrayJoiner = ' ' }: NormalizeSearchTextOptions = {}
|
||||
): string {
|
||||
if (value === null || value === undefined) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const normalized = value
|
||||
.map(item =>
|
||||
normalizeSearchText(item, {
|
||||
fallback: '',
|
||||
arrayJoiner,
|
||||
})
|
||||
)
|
||||
.filter(Boolean);
|
||||
|
||||
return normalized.length > 0 ? normalized.join(arrayJoiner) : fallback;
|
||||
}
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
const parsed = tryParseSerializedText(value);
|
||||
if (parsed === null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof parsed === 'string' || Array.isArray(parsed)) {
|
||||
return normalizeSearchText(parsed, {
|
||||
fallback,
|
||||
arrayJoiner,
|
||||
});
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
Reference in New Issue
Block a user