Skip to content
Open
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
29 changes: 26 additions & 3 deletions indexer-api-gateway/src/api/services/search.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Controller, HttpCode, HttpStatus, Get, Query } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiQuery, ApiInternalServerErrorResponse } from '@nestjs/swagger';
import { Controller, HttpCode, HttpStatus, Get, Post, Body, Query } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiQuery, ApiInternalServerErrorResponse, ApiBody, ApiOkResponse } from '@nestjs/swagger';
import { IndexerMessageAPI } from '@indexer/common';
import { ApiClient } from '../api-client.js';
import { ApiPaginatedResponse } from '#decorators';
import { InternalServerErrorDTO, SearchItemDTO } from '#dto';
import { InternalServerErrorDTO, SearchItemDTO, AdvancedSearchParamsDTO, AdvancedSearchResultDTO } from '#dto';

@Controller('search')
@ApiTags('search')
Expand Down Expand Up @@ -52,4 +52,27 @@ export class SearchApi extends ApiClient {
pageSize
});
}

@ApiOperation({
summary: 'Advanced Search',
description:
'Multi-step, multi-condition search across Indexer documents. ' +
'Supports exact match, substring, regex, range, and set operators. ' +
'Steps can cross-reference field values from previous steps. ' +
'The response includes configurable display columns and a serialised ' +
'search token that can be saved as a URL bookmark.',
})
@ApiBody({ type: AdvancedSearchParamsDTO })
@ApiOkResponse({ type: AdvancedSearchResultDTO })
@ApiInternalServerErrorResponse({
description: 'Internal server error',
type: InternalServerErrorDTO,
})
@Post('/advanced')
@HttpCode(HttpStatus.OK)
async advancedSearch(
@Body() body: AdvancedSearchParamsDTO
): Promise<AdvancedSearchResultDTO> {
return await this.send(IndexerMessageAPI.GET_ADVANCED_SEARCH_API, body);
}
}
177 changes: 177 additions & 0 deletions indexer-api-gateway/src/dto/advanced-search.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import {
AdvancedSearchParams,
AdvancedSearchResult,
AdvancedSearchResultItem,
AdvancedSearchStep,
AdvancedSearchDisplayColumn,
AdvancedSearchSort,
AdvancedSearchGroupBy,
SearchCondition,
ConditionOperator,
} from '@indexer/interfaces';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
IsString,
IsOptional,
IsNumber,
IsArray,
IsIn,
ValidateNested,
IsBoolean,
} from 'class-validator';
import { Type } from 'class-transformer';

const OPERATORS: ConditionOperator[] = [
'eq', 'neq', 'contains', 'regex', 'gt', 'gte', 'lt', 'lte',
'between', 'in', 'not_in',
];

export class SearchConditionDTO implements SearchCondition {
@ApiProperty({ description: 'Field path (dot notation)', example: 'analytics.policyId' })
@IsString()
field: string;

@ApiProperty({ description: 'Operator', enum: OPERATORS })
@IsIn(OPERATORS)
operator: ConditionOperator;

@ApiProperty({ description: 'Comparison value' })
value: string | number | boolean | string[];

@ApiPropertyOptional({ description: 'Upper bound for "between" operator' })
@IsOptional()
valueTo?: string | number;
}

export class AdvancedSearchStepDTO implements AdvancedSearchStep {
@ApiPropertyOptional({ description: 'Step label', example: 'Step 1: Monitoring Reports' })
@IsString()
@IsOptional()
label?: string;

@ApiPropertyOptional({ description: 'Document type to restrict this step', example: 'VC-Document' })
@IsString()
@IsOptional()
type?: string;

@ApiPropertyOptional({ description: 'Multiple document types (OR)', type: [String] })
@IsArray()
@IsString({ each: true })
@IsOptional()
types?: string[];

@ApiProperty({ description: 'AND-combined field conditions', type: [SearchConditionDTO] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => SearchConditionDTO)
conditions: SearchConditionDTO[];

@ApiPropertyOptional({ description: 'Fields to carry forward to the next step', type: [String] })
@IsArray()
@IsString({ each: true })
@IsOptional()
carryFields?: string[];
}

export class AdvancedSearchDisplayColumnDTO implements AdvancedSearchDisplayColumn {
@ApiProperty({ description: 'Field path', example: 'analytics.policyId' })
@IsString()
field: string;

@ApiProperty({ description: 'Column header', example: 'Policy ID' })
@IsString()
header: string;
}

export class AdvancedSearchSortDTO implements AdvancedSearchSort {
@ApiProperty({ description: 'Sort field', example: 'consensusTimestamp' })
@IsString()
field: string;

@ApiProperty({ description: 'Sort direction', enum: ['asc', 'desc'] })
@IsIn(['asc', 'desc'])
order: 'asc' | 'desc';
}

export class AdvancedSearchGroupByDTO implements AdvancedSearchGroupBy {
@ApiProperty({ description: 'Group-by field', example: 'analytics.policyId' })
@IsString()
field: string;

@ApiPropertyOptional({ description: 'Optional numeric/date ranges', type: 'array', items: { type: 'object' } })
@IsArray()
@IsOptional()
ranges?: any[];
}

export class AdvancedSearchParamsDTO implements AdvancedSearchParams {
@ApiProperty({ description: 'Ordered search steps', type: [AdvancedSearchStepDTO] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => AdvancedSearchStepDTO)
steps: AdvancedSearchStepDTO[];

@ApiPropertyOptional({ description: 'Result grid columns', type: [AdvancedSearchDisplayColumnDTO] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => AdvancedSearchDisplayColumnDTO)
@IsOptional()
displayColumns?: AdvancedSearchDisplayColumnDTO[];

@ApiPropertyOptional({ description: 'Sort configuration', type: [AdvancedSearchSortDTO] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => AdvancedSearchSortDTO)
@IsOptional()
sort?: AdvancedSearchSortDTO[];

@ApiPropertyOptional({ description: 'Group-by configuration', type: AdvancedSearchGroupByDTO })
@ValidateNested()
@Type(() => AdvancedSearchGroupByDTO)
@IsOptional()
groupBy?: AdvancedSearchGroupByDTO;

@ApiProperty({ description: 'Page index', example: 0 })
@IsNumber()
pageIndex: number;

@ApiProperty({ description: 'Page size (max 100)', example: 10 })
@IsNumber()
pageSize: number;
}

export class AdvancedSearchResultItemDTO implements AdvancedSearchResultItem {
@ApiProperty({ description: 'Consensus timestamp', example: '1706823227.586179534' })
consensusTimestamp: string;

@ApiProperty({ description: 'Document type', example: 'VC-Document' })
type: string;

@ApiPropertyOptional({ description: 'Topic ID' })
topicId?: string;

@ApiPropertyOptional({ description: 'Owner DID' })
owner?: string;

[key: string]: any;
}

export class AdvancedSearchResultDTO implements AdvancedSearchResult {
@ApiProperty({ description: 'Result rows', type: [AdvancedSearchResultItemDTO] })
items: AdvancedSearchResultItemDTO[];

@ApiProperty({ description: 'Total matching documents', example: 42 })
total: number;

@ApiProperty({ description: 'Current page index', example: 0 })
pageIndex: number;

@ApiProperty({ description: 'Page size', example: 10 })
pageSize: number;

@ApiProperty({ description: 'Column metadata for the grid', type: 'array', items: { type: 'object' } })
columns: Array<{ field: string; header: string }>;

@ApiPropertyOptional({ description: 'Base64-encoded search token for bookmarking' })
searchToken?: string;
}
3 changes: 2 additions & 1 deletion indexer-api-gateway/src/dto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export * from './schema-tree.dto.js';
export * from './internal-server-error.dto.js';
export * from './search-policy.dto.js';
export * from './message.dto.js';
export * from './set-loading-priority.dto.js';
export * from './set-loading-priority.dto.js';
export * from './advanced-search.dto.js';
1 change: 1 addition & 0 deletions indexer-common/src/messages/message-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export enum IndexerMessageAPI {
GET_LOG_TOKENS = 'INDEXER_API_GET_LOG_TOKENS',
GET_LOG_NFTS = 'INDEXER_API_GET_LOG_NFTS',
GET_SEARCH_API = 'INDEXER_API_GET_SEARCH_API',
GET_ADVANCED_SEARCH_API = 'INDEXER_API_GET_ADVANCED_SEARCH_API',
GET_IPFS_FILE = 'INDEXER_API_GET_IPFS_FILE',

// #region LANDING
Expand Down
2 changes: 2 additions & 0 deletions indexer-frontend/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { DocumentsComponent } from '@dev/logs/documents/documents.component';

//Home
import { SearchViewComponent } from '@views/search/search.component';
import { AdvancedSearchViewComponent } from '@views/search/advanced/advanced-search.component';
import { HomeComponent } from '@views/home/home.component';

//Details
Expand Down Expand Up @@ -64,6 +65,7 @@ export const routes: Routes = [
//Home
{ path: '', component: HomeComponent },
{ path: 'search', component: SearchViewComponent },
{ path: 'search/advanced', component: AdvancedSearchViewComponent },
{ path: 'priority-queue', component: PriorityQueueComponent },

//Collections
Expand Down
11 changes: 10 additions & 1 deletion indexer-frontend/src/app/services/search.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { API_BASE_URL } from './api';
import { ApiUtils } from './utils';
import { Page, PageFilters, SearchItem } from '@indexer/interfaces';
import { Page, SearchItem, AdvancedSearchParams, AdvancedSearchResult } from '@indexer/interfaces';

/**
* Services for working from search.
Expand All @@ -21,4 +21,13 @@ export class SearchService {
const options = ApiUtils.getOptions({ search: data, ...filters });
return this.http.get<Page<SearchItem>>(this.url, options) as any;
}

public advancedSearch(
params: AdvancedSearchParams
): Observable<AdvancedSearchResult> {
return this.http.post<AdvancedSearchResult>(
`${this.url}/advanced`,
params
);
}
}
Loading
Loading