-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update searchVector at label identifier update for custom fields #7588
Conversation
); | ||
|
||
// index needs to be recreated as typeorm deletes then recreates searchVector column at alter | ||
await this.indexMetadataService.createIndex( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Weiko what happens in that typeOrm deletes then recreates searchVector column during update (cf PostGresQuerryRunner - changeColumn method - L751:
if (oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length ||
newColumn.isArray !== oldColumn.isArray ||
(!oldColumn.generatedType &&
newColumn.generatedType === "STORED") ||
(oldColumn.asExpression !== newColumn.asExpression &&
newColumn.generatedType === "STORED")) {
// To avoid data conversion, we just recreate column
await this.dropColumn(table, oldColumn);
await this.addColumn(table, newColumn);
// update cloned table
clonedTable = table.clone();
}
```)
So the index on the table is deleted as well, but the index metadata is untouched, creating a temporary inconsistancy.
When I recreate the index right after, nothing new is saved on the index metadata table as the same information are to be saved, and the index is re-created on the table, so we are back to a consistent state.
Does that sound too fragile? This could also be a potential candidate for another patch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting, thanks for the details! LGTM.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR Summary
This pull request enhances the search functionality for custom fields by updating the searchVector when the label identifier is modified.
- Introduced new
SearchModule
andSearchService
to handle search vector updates - Updated
FieldMetadataService
andObjectMetadataService
to integrate with the new search functionality - Modified
WorkspaceMigrationRunnerService
to support generated columns for searchVector - Added utility functions in
is-searchable-field.util.ts
to determine searchable field types - Improved
getTsVectorColumnExpressionFromFields
to filter and validate searchable fields
9 file(s) reviewed, 10 comment(s)
Edit PR Review Bot Settings | Greptile
{ | ||
action: WorkspaceMigrationColumnActionType.ALTER, | ||
currentColumnDefinition: { | ||
columnName: currentFieldMetadata.name, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: consider using computeColumnName(currentFieldMetadata) for consistency with handleCreateAction
defaultValue: undefined, | ||
}, | ||
alteredColumnDefinition: { | ||
columnName: alteredFieldMetadata.name, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: consider using computeColumnName(alteredFieldMetadata) for consistency with handleCreateAction
const filteredFieldsUsedForSearch = fieldsUsedForSearch.filter((field) => | ||
isSearchableFieldType(field.type), | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: This filtering is redundant since the input type is already constrained to SearchableFieldType
const columnExpressions = fieldsUsedForSearch.flatMap( | ||
getColumnExpressionsFromField, | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: Use filteredFieldsUsedForSearch instead of fieldsUsedForSearch here
if (filteredFieldsUsedForSearch.length < 1) { | ||
throw new Error('No searchable fields found'); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Consider using a more specific error type, such as WorkspaceMigrationException
const SEARCHABLE_FIELD_TYPES = [ | ||
FieldMetadataType.TEXT, | ||
FieldMetadataType.FULL_NAME, | ||
FieldMetadataType.EMAILS, | ||
FieldMetadataType.ADDRESS, | ||
FieldMetadataType.LINKS, | ||
] as const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Consider using an enum or object for SEARCHABLE_FIELD_TYPES to improve type safety and maintainability
this.logger.log(`Running command for workspace ${workspaceId}`); | ||
|
||
try { | ||
const searchVectorFields = await this.fieldMetadataRepository.findBy({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Weiko I could add some very specific logic here to filter out fields whose entry in _typeorm_generated_columns_and_materialized_views has a value containing "CASE" (it would imply declaring/creating the repository etc for this entity that we kind of decided to set aside so far). This would make the command idempotent, while it is not completely the case here - if we run the command multiple times the output will be the same but we will have re-created the column and index.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's fine recreating everything as it will be executed only once. We can even go further and truncate the _typeorm_generated_columns_and_materialized_views table
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea, as we just discussed I will do the truncate manually on the db so I can first test on a workspace independently.
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; | ||
import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module'; | ||
|
||
@Module({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So far I did not added this command to an upgrade-0.32 set of command as we would only want to run it twice and not to run it for self hosting
this.logger.log(`Running command for workspace ${workspaceId}`); | ||
|
||
try { | ||
const searchVectorFields = await this.fieldMetadataRepository.findBy({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's fine recreating everything as it will be executed only once. We can even go further and truncate the _typeorm_generated_columns_and_materialized_views table
workspaceId, | ||
); | ||
|
||
if (isSearchEnabled && isWorkspaceMigratedForSearch) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: remove feature flag in next PR, shouldn't be necessary anymore
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done in this PR !
packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts
Outdated
Show resolved
Hide resolved
packages/twenty-server/src/engine/metadata-modules/search/search.service.ts
Outdated
Show resolved
Hide resolved
packages/twenty-server/src/engine/metadata-modules/search/search.service.ts
Outdated
Show resolved
Hide resolved
...(isDefined(indexType) ? { indexType: indexType } : {}), | ||
isCustom: isCustom, | ||
}); | ||
result = await this.indexMetadataRepository.upsert( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we rename the method upsertIndex? @ijreilly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wouldn't - it still creates the index, the upsert only concerns the entry in metadata table. but a migration to create the index is added anyway
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right @ijreilly!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, good work!
By default, when custom fields are created, a searchVector field is created based on the "name" field, which is also the label identifier by default.
When this label identifier is updated, we want to update the searchVector field to use this field as searchable field instead, if it is of "searchable type" (today it is only possible to select a text or number field as label identifier, while number fields are not searchable).