mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: support multiple terms in search, remove tag support in search (#5395)
1. Removing tag support in search 2. Adding multi keyword support for search
This commit is contained in:
		
							parent
							
								
									5414fa6663
								
							
						
					
					
						commit
						432aed3034
					
				@ -84,7 +84,13 @@ export default class FeatureSearchController extends Controller {
 | 
			
		||||
                favoritesFirst,
 | 
			
		||||
            } = req.query;
 | 
			
		||||
            const userId = req.user.id;
 | 
			
		||||
            const normalizedTag = tag?.map((tag) => tag.split(':'));
 | 
			
		||||
            const normalizedQuery = query
 | 
			
		||||
                ?.split(',')
 | 
			
		||||
                .map((query) => query.trim())
 | 
			
		||||
                .filter((query) => query);
 | 
			
		||||
            const normalizedTag = tag
 | 
			
		||||
                ?.map((tag) => tag.split(':'))
 | 
			
		||||
                .filter((tag) => tag.length === 2);
 | 
			
		||||
            const normalizedStatus = status
 | 
			
		||||
                ?.map((tag) => tag.split(':'))
 | 
			
		||||
                .filter(
 | 
			
		||||
@ -100,7 +106,7 @@ export default class FeatureSearchController extends Controller {
 | 
			
		||||
                sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';
 | 
			
		||||
            const normalizedFavoritesFirst = favoritesFirst === 'true';
 | 
			
		||||
            const { features, total } = await this.featureSearchService.search({
 | 
			
		||||
                query,
 | 
			
		||||
                queryParams: normalizedQuery,
 | 
			
		||||
                projectId,
 | 
			
		||||
                type,
 | 
			
		||||
                userId,
 | 
			
		||||
 | 
			
		||||
@ -210,50 +210,6 @@ test('should filter features by environment status', async () => {
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('should filter by partial tag', async () => {
 | 
			
		||||
    await app.createFeature('my_feature_a');
 | 
			
		||||
    await app.createFeature('my_feature_b');
 | 
			
		||||
    await app.addTag('my_feature_a', {
 | 
			
		||||
        type: 'simple',
 | 
			
		||||
        value: 'my_tag',
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const { body } = await filterFeaturesByTag(['simple']);
 | 
			
		||||
 | 
			
		||||
    expect(body).toMatchObject({
 | 
			
		||||
        features: [{ name: 'my_feature_a' }],
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('should search matching features by tag', async () => {
 | 
			
		||||
    await app.createFeature('my_feature_a');
 | 
			
		||||
    await app.createFeature('my_feature_b');
 | 
			
		||||
    await app.addTag('my_feature_a', {
 | 
			
		||||
        type: 'simple',
 | 
			
		||||
        value: 'my_tag',
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const { body: fullMatch } = await searchFeatures({
 | 
			
		||||
        query: 'simple:my_tag',
 | 
			
		||||
    });
 | 
			
		||||
    const { body: tagTypeMatch } = await searchFeatures({ query: 'simple' });
 | 
			
		||||
    const { body: tagValueMatch } = await searchFeatures({ query: 'my_tag' });
 | 
			
		||||
    const { body: partialTagMatch } = await searchFeatures({ query: 'e:m' });
 | 
			
		||||
 | 
			
		||||
    expect(fullMatch).toMatchObject({
 | 
			
		||||
        features: [{ name: 'my_feature_a' }],
 | 
			
		||||
    });
 | 
			
		||||
    expect(tagTypeMatch).toMatchObject({
 | 
			
		||||
        features: [{ name: 'my_feature_a' }],
 | 
			
		||||
    });
 | 
			
		||||
    expect(tagValueMatch).toMatchObject({
 | 
			
		||||
        features: [{ name: 'my_feature_a' }],
 | 
			
		||||
    });
 | 
			
		||||
    expect(partialTagMatch).toMatchObject({
 | 
			
		||||
        features: [{ name: 'my_feature_a' }],
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('should return all feature tags', async () => {
 | 
			
		||||
    await app.createFeature('my_feature_a');
 | 
			
		||||
    await app.addTag('my_feature_a', {
 | 
			
		||||
@ -500,3 +456,31 @@ test('should search features by description', async () => {
 | 
			
		||||
        features: [{ name: 'my_feature_b', description }],
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('should support multiple search values', async () => {
 | 
			
		||||
    const description = 'secretdescription';
 | 
			
		||||
    await app.createFeature('my_feature_a');
 | 
			
		||||
    await app.createFeature({ name: 'my_feature_b', description });
 | 
			
		||||
    await app.createFeature('my_feature_c');
 | 
			
		||||
 | 
			
		||||
    const { body } = await searchFeatures({
 | 
			
		||||
        query: 'descr,c',
 | 
			
		||||
    });
 | 
			
		||||
    expect(body).toMatchObject({
 | 
			
		||||
        features: [
 | 
			
		||||
            { name: 'my_feature_b', description },
 | 
			
		||||
            { name: 'my_feature_c' },
 | 
			
		||||
        ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const { body: emptyQuery } = await searchFeatures({
 | 
			
		||||
        query: ' , ',
 | 
			
		||||
    });
 | 
			
		||||
    expect(emptyQuery).toMatchObject({
 | 
			
		||||
        features: [
 | 
			
		||||
            { name: 'my_feature_a' },
 | 
			
		||||
            { name: 'my_feature_b' },
 | 
			
		||||
            { name: 'my_feature_c' },
 | 
			
		||||
        ],
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -529,7 +529,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
 | 
			
		||||
    async searchFeatures({
 | 
			
		||||
        projectId,
 | 
			
		||||
        userId,
 | 
			
		||||
        query: queryString,
 | 
			
		||||
        queryParams,
 | 
			
		||||
        type,
 | 
			
		||||
        tag,
 | 
			
		||||
        status,
 | 
			
		||||
@ -542,9 +542,6 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
 | 
			
		||||
        features: IFeatureOverview[];
 | 
			
		||||
        total: number;
 | 
			
		||||
    }> {
 | 
			
		||||
        const normalizedFullTag = tag?.filter((tag) => tag.length === 2);
 | 
			
		||||
        const normalizedHalfTag = tag?.filter((tag) => tag.length === 1).flat();
 | 
			
		||||
 | 
			
		||||
        const validatedSortOrder =
 | 
			
		||||
            sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';
 | 
			
		||||
 | 
			
		||||
@ -554,54 +551,33 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
 | 
			
		||||
                if (projectId) {
 | 
			
		||||
                    query.where({ project: projectId });
 | 
			
		||||
                }
 | 
			
		||||
                const hasQueryString = Boolean(queryString?.trim());
 | 
			
		||||
                const hasHalfTag =
 | 
			
		||||
                    normalizedHalfTag && normalizedHalfTag.length > 0;
 | 
			
		||||
                if (hasQueryString || hasHalfTag) {
 | 
			
		||||
                    const tagQuery = this.db
 | 
			
		||||
                        .from('feature_tag')
 | 
			
		||||
                        .select('feature_name');
 | 
			
		||||
                    // todo: we can run a cheaper query when no colon is detected
 | 
			
		||||
                const hasQueryString = queryParams?.length;
 | 
			
		||||
 | 
			
		||||
                if (hasQueryString) {
 | 
			
		||||
                        tagQuery.whereRaw("(?? || ':' || ??) ILIKE ?", [
 | 
			
		||||
                            'tag_type',
 | 
			
		||||
                            'tag_value',
 | 
			
		||||
                            `%${queryString}%`,
 | 
			
		||||
                        ]);
 | 
			
		||||
                    }
 | 
			
		||||
                    if (hasHalfTag) {
 | 
			
		||||
                        const tagParameters = normalizedHalfTag.map(
 | 
			
		||||
                            (tag) => `%${tag}%`,
 | 
			
		||||
                    const sqlParameters = queryParams.map(
 | 
			
		||||
                        (item) => `%${item}%`,
 | 
			
		||||
                    );
 | 
			
		||||
                        const tagQueryParameters = normalizedHalfTag
 | 
			
		||||
                    const sqlQueryParameters = sqlParameters
 | 
			
		||||
                        .map(() => '?')
 | 
			
		||||
                        .join(',');
 | 
			
		||||
                        tagQuery
 | 
			
		||||
                            .orWhereRaw(
 | 
			
		||||
                                `(??) ILIKE ANY (ARRAY[${tagQueryParameters}])`,
 | 
			
		||||
                                ['tag_type', ...tagParameters],
 | 
			
		||||
                            )
 | 
			
		||||
                            .orWhereRaw(
 | 
			
		||||
                                `(??) ILIKE ANY (ARRAY[${tagQueryParameters}])`,
 | 
			
		||||
                                ['tag_value', ...tagParameters],
 | 
			
		||||
                            );
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    query.where((builder) => {
 | 
			
		||||
                        builder
 | 
			
		||||
                            .whereILike('features.name', `%${queryString}%`)
 | 
			
		||||
                            .orWhereILike(
 | 
			
		||||
                                'features.description',
 | 
			
		||||
                                `%${queryString}%`,
 | 
			
		||||
                            .orWhereRaw(
 | 
			
		||||
                                `(??) ILIKE ANY (ARRAY[${sqlQueryParameters}])`,
 | 
			
		||||
                                ['features.name', ...sqlParameters],
 | 
			
		||||
                            )
 | 
			
		||||
                            .orWhereIn('features.name', tagQuery);
 | 
			
		||||
                            .orWhereRaw(
 | 
			
		||||
                                `(??) ILIKE ANY (ARRAY[${sqlQueryParameters}])`,
 | 
			
		||||
                                ['features.description', ...sqlParameters],
 | 
			
		||||
                            );
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
                if (normalizedFullTag && normalizedFullTag.length > 0) {
 | 
			
		||||
                if (tag && tag.length > 0) {
 | 
			
		||||
                    const tagQuery = this.db
 | 
			
		||||
                        .from('feature_tag')
 | 
			
		||||
                        .select('feature_name')
 | 
			
		||||
                        .whereIn(['tag_type', 'tag_value'], normalizedFullTag);
 | 
			
		||||
                        .whereIn(['tag_type', 'tag_value'], tag);
 | 
			
		||||
                    query.whereIn('features.name', tagQuery);
 | 
			
		||||
                }
 | 
			
		||||
                if (type) {
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,7 @@ export interface FeatureConfigurationClient {
 | 
			
		||||
 | 
			
		||||
export interface IFeatureSearchParams {
 | 
			
		||||
    userId: number;
 | 
			
		||||
    query?: string;
 | 
			
		||||
    queryParams?: string[];
 | 
			
		||||
    projectId?: string;
 | 
			
		||||
    type?: string[];
 | 
			
		||||
    tag?: string[][];
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user