Skip to content

Commit

Permalink
feat: support search hits highlight
Browse files Browse the repository at this point in the history
  • Loading branch information
caoxing9 committed Nov 19, 2024
1 parent 4cc6a8b commit cec1470
Show file tree
Hide file tree
Showing 10 changed files with 331 additions and 88 deletions.
91 changes: 12 additions & 79 deletions apps/nestjs-backend/src/features/aggregation/aggregation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,12 @@ export class AggregationService {
throw new BadRequestException('Search query is required');
}

const searchFields = await this.getSearchFields(fieldInstanceMap, search, viewId, projection);
const searchFields = await this.recordService.getSearchFields(
fieldInstanceMap,
search,
viewId,
projection
);

const queryBuilder = this.knex(dbFieldName);
this.dbProvider.searchCountQuery(queryBuilder, searchFields, search[0]);
Expand Down Expand Up @@ -629,7 +634,12 @@ export class AggregationService {
throw new BadRequestException('Search query is required');
}

const searchFields = await this.getSearchFields(fieldInstanceMap, search, viewId, projection);
const searchFields = await this.recordService.getSearchFields(
fieldInstanceMap,
search,
viewId,
projection
);

const { queryBuilder: viewRecordsQB } = await this.recordService.buildFilterSortQuery(
tableId,
Expand Down Expand Up @@ -715,81 +725,4 @@ export class AggregationService {
};
});
}

private async getSearchFields(
originFieldInstanceMap: Record<string, IFieldInstance>,
search?: [string, string?, boolean?],
viewId?: string,
projection?: string[]
) {
let viewColumnMeta: IGridColumnMeta | null = null;
const fieldInstanceMap = { ...originFieldInstanceMap };

if (viewId) {
const { columnMeta: viewColumnRawMeta } =
(await this.prisma.view.findUnique({
where: { id: viewId, deletedTime: null },
select: { columnMeta: true },
})) || {};

viewColumnMeta = viewColumnRawMeta ? JSON.parse(viewColumnRawMeta) : null;

if (viewColumnMeta) {
Object.entries(viewColumnMeta).forEach(([key, value]) => {
if (get(value, ['hidden'])) {
delete fieldInstanceMap[key];
}
});
}
}

if (projection?.length) {
Object.keys(fieldInstanceMap).forEach((fieldId) => {
if (!projection.includes(fieldId)) {
delete fieldInstanceMap[fieldId];
}
});
}

return orderBy(
Object.values(fieldInstanceMap)
.map((field) => ({
...field,
isStructuredCellValue: field.isStructuredCellValue,
}))
.filter((field) => {
if (!viewColumnMeta) {
return true;
}
return !viewColumnMeta?.[field.id]?.hidden;
})
.filter((field) => {
if (!projection) {
return true;
}
return projection.includes(field.id);
})
.filter((field) => {
if (!search?.[1]) {
return true;
}

const searchArr = search[1].split(',');
return searchArr.includes(field.id);
})
.filter((field) => {
if (field.type === FieldType.Checkbox) {
return false;
}
return true;
})
.map((field) => {
return {
...field,
order: viewColumnMeta?.[field.id]?.order ?? Number.MIN_SAFE_INTEGER,
};
}),
['order', 'createTime']
) as unknown as IFieldInstance[];
}
}
145 changes: 144 additions & 1 deletion apps/nestjs-backend/src/features/record/record.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
IExtraResult,
IFilter,
IFilterSet,
IGridColumnMeta,
IGroup,
ILinkCellValue,
IRecord,
Expand Down Expand Up @@ -47,9 +48,10 @@ import type {
} from '@teable/openapi';
import { GroupPointType, UploadType } from '@teable/openapi';
import { Knex } from 'knex';
import { get, difference, keyBy } from 'lodash';
import { get, difference, keyBy, orderBy } from 'lodash';
import { InjectModel } from 'nest-knexjs';
import { ClsService } from 'nestjs-cls';
import { K } from 'vitest/dist/reporters-yx5ZTtEV';
import { CacheService } from '../../cache/cache.service';
import { ThresholdConfig, IThresholdConfig } from '../../configs/threshold.config';
import { InjectDbProvider } from '../../db-provider/db.provider';
Expand Down Expand Up @@ -1347,9 +1349,150 @@ export class RecordService {
.$queryRawUnsafe<{ __id: string }[]>(queryBuilder.toQuery());
const ids = result.map((r) => r.__id);

// this search step should not abort the query
try {
const searchHitIndex = await this.getSearchHitIndex(queryBuilder, tableId, query);
return { ids, extra: { groupPoints, searchHitIndex } };
} catch (e) {
this.logger.error('Get search index error:', e);
}

return { ids, extra: { groupPoints } };
}

async getSearchFields(
originFieldInstanceMap: Record<string, IFieldInstance>,
search?: [string, string?, boolean?],
viewId?: string,
projection?: string[]
) {
let viewColumnMeta: IGridColumnMeta | null = null;
const fieldInstanceMap = { ...originFieldInstanceMap };

if (viewId) {
const { columnMeta: viewColumnRawMeta } =
(await this.prismaService.view.findUnique({
where: { id: viewId, deletedTime: null },
select: { columnMeta: true },
})) || {};

viewColumnMeta = viewColumnRawMeta ? JSON.parse(viewColumnRawMeta) : null;

if (viewColumnMeta) {
Object.entries(viewColumnMeta).forEach(([key, value]) => {
if (get(value, ['hidden'])) {
delete fieldInstanceMap[key];
}
});
}
}

if (projection?.length) {
Object.keys(fieldInstanceMap).forEach((fieldId) => {
if (!projection.includes(fieldId)) {
delete fieldInstanceMap[fieldId];
}
});
}

return orderBy(
Object.values(fieldInstanceMap)
.map((field) => ({
...field,
isStructuredCellValue: field.isStructuredCellValue,
}))
.filter((field) => {
if (!viewColumnMeta) {
return true;
}
return !viewColumnMeta?.[field.id]?.hidden;
})
.filter((field) => {
if (!projection) {
return true;
}
return projection.includes(field.id);
})
.filter((field) => {
if (!search?.[1]) {
return true;
}

const searchArr = search[1].split(',');
return searchArr.includes(field.id);
})
.filter((field) => {
if (field.type === FieldType.Checkbox) {
return false;
}
return true;
})
.map((field) => {
return {
...field,
order: viewColumnMeta?.[field.id]?.order ?? Number.MIN_SAFE_INTEGER,
};
}),
['order', 'createTime']
) as unknown as IFieldInstance[];
}

private async getSearchHitIndex(
recordQuery: Knex.QueryBuilder,
tableId: string,
query: IGetRecordsRo
) {
const { search, viewId, projection } = query;

if (!search) {
return null;
}

const fieldsRaw = await this.prismaService.field.findMany({
where: { tableId, deletedTime: null },
});
const fieldInstances = fieldsRaw.map((field) => createFieldInstanceByRaw(field));
const fieldInstanceMap = fieldInstances.reduce(
(map, field) => {
map[field.id] = field;
return map;
},
{} as Record<string, IFieldInstance>
);
recordQuery.clearSelect();
const searchFields = await this.getSearchFields(fieldInstanceMap, search, viewId, projection);
const newQuery = this.knex
.with('record_range', (qb) => {
qb.select('*').from(recordQuery.select('*').as('record_range'));
})
.with('search_index', (qb) => {
this.dbProvider.searchIndexQuery(qb, searchFields, search?.[0], 'record_range');
})
.from('search_index');

const cases = searchFields.map((field, index) => {
return this.knex.raw(`CASE WHEN ?? = ? THEN ? END`, [
'matched_column',
field.dbFieldName,
index + 1,
]);
});
cases.length && newQuery.orderByRaw(cases.join(','));

const result = await this.prismaService.$queryRawUnsafe<{ __id: string; fieldId: string }[]>(
newQuery.toQuery()
);

if (!result.length) {
return null;
}

return result.map((res) => ({
fieldId: res.fieldId,
recordId: res.__id,
}));
}

async getRecordsFields(
tableId: string,
query: IGetRecordsRo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export const GridViewBaseInner: React.FC<IGridViewBaseInnerProps> = (
generateLocalId(tableId, activeViewId)
);

const { onVisibleRegionChanged, onReset, recordMap, groupPoints, recordsQuery } =
const { onVisibleRegionChanged, onReset, recordMap, groupPoints, recordsQuery, searchHitIndex } =
useGridAsyncRecords(ssrRecords, undefined, viewQuery, groupPointsServerData);

const commentCountMap = useCommentCountMap(recordsQuery);
Expand Down Expand Up @@ -795,6 +795,7 @@ export const GridViewBaseInner: React.FC<IGridViewBaseInnerProps> = (
groupPoints={groupPoints as unknown as IGroupPoint[]}
collaborators={collaborators}
searchCursor={searchCursor}
searchHitIndex={searchHitIndex}
getCellContent={getCellContent}
onDelete={getAuthorizedFunction(onDelete, 'record|update')}
onDragStart={onDragStart}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { IRecord } from '@teable/core';
import type { IGetRecordsRo, IGroupPointsVo } from '@teable/openapi';
import { inRange, debounce } from 'lodash';
import { inRange, debounce, get } from 'lodash';
import { useCallback, useEffect, useRef, useState, useMemo } from 'react';
import type { IGridProps, IRectangle } from '../..';
import { useRecords } from '../../../hooks/use-records';
Expand All @@ -12,6 +12,7 @@ const defaultVisiblePages = { x: 0, y: 0, width: 0, height: 0 };

type IRes = {
groupPoints: IGroupPointsVo | null;
searchHitIndex?: { fieldId: string; recordId: string }[];
recordMap: IRecordIndexMap;
onReset: () => void;
onForceUpdate: () => void;
Expand Down Expand Up @@ -137,5 +138,6 @@ export const useGridAsyncRecords = (
recordsQuery,
onForceUpdate,
onReset,
searchHitIndex: get(extra, 'searchHitIndex'),
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const darkTheme = {

// search cursor
searchCursorBg: hexToRGBA(colors.orange[400]),
searchTargetIndexBg: hexToRGBA(colors.yellow[400]),

// comment
commentCountBg: colors.orange[400],
Expand Down
4 changes: 4 additions & 0 deletions packages/sdk/src/components/grid/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export interface IGridExternalProps {
collaborators?: ICollaborator;
// [rowIndex, colIndex]
searchCursor?: [number, number] | null;
searchHitIndex?: { fieldId: string; recordId: string }[];

/**
* Indicates which areas can be dragged, including rows, columns or no drag
Expand Down Expand Up @@ -188,6 +189,7 @@ const GridBase: ForwardRefRenderFunction<IGridRef, IGridProps> = (props, forward
customIcons,
collaborators,
searchCursor,
searchHitIndex,
groupPoints,
columnHeaderVisible = true,
getCellContent,
Expand Down Expand Up @@ -549,6 +551,7 @@ const GridBase: ForwardRefRenderFunction<IGridRef, IGridProps> = (props, forward
rowControls={rowControls}
collaborators={collaborators}
searchCursor={searchCursor}
searchHitIndex={searchHitIndex}
imageManager={imageManager}
spriteManager={spriteManager}
coordInstance={coordInstance}
Expand Down Expand Up @@ -586,6 +589,7 @@ const GridBase: ForwardRefRenderFunction<IGridRef, IGridProps> = (props, forward
selectable={selectable}
collaborators={collaborators}
searchCursor={searchCursor}
searchHitIndex={searchHitIndex}
rowControls={rowControls}
imageManager={imageManager}
spriteManager={spriteManager}
Expand Down
2 changes: 2 additions & 0 deletions packages/sdk/src/components/grid/InteractionLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export const InteractionLayerBase: ForwardRefRenderFunction<
collapsedGroupIds,
collaborators,
searchCursor,
searchHitIndex,
activeCell,
getLinearRow,
real2RowIndex,
Expand Down Expand Up @@ -722,6 +723,7 @@ export const InteractionLayerBase: ForwardRefRenderFunction<
visibleRegion={visibleRegion}
collaborators={collaborators}
searchCursor={searchCursor}
searchHitIndex={searchHitIndex}
activeCellBound={activeCellBound}
activeCell={activeCell}
mouseState={mouseState}
Expand Down
Loading

0 comments on commit cec1470

Please sign in to comment.