The search component in 4.34 does not seem to honor exactMatch:False. It will only honor it if the search is in the first characters. Say I am looking for "Driscoll" if I search 'ris' it will not come up, but if I do 'Dri' it will.
In 4.34 the exactMatch: false → prefix only. Where in 4.21 it's like: LIKE '%ris%'
// Wait for your view to be ready
const quickSearch = document.getElementById("quickSearch");
await quickSearch.componentOnReady();
// Assign the map view
quickSearch.view = view;
// Configure only your parcel layer sources
quickSearch.sources = [
{
layer: parcelLayer,
searchFields: ["PartyName_1"],
name: "Search by Owner",
exactMatch: false,
outFields: ["PartyName_1"],
displayField: "PartyName_1",
suggestionsEnabled: true,
},
{
layer: parcelLayer,
searchFields: ["PropertyAddress"],
name: "Search by Address",
outFields: ["PropertyAddress"],
displayField: "PropertyAddress",
exactMatch: false,
},
{
layer: parcelLayer,
searchFields: ["QuickRefID_1"],
name: "Search by Quick Reference ID",
outFields: ["QuickRefID_1"],
displayField: "QuickRefID_1",
exactMatch: false,
}
];
Solved! Go to Solution.
@MatthewDriscoll This is the expected behavior. The change was introduced in 4.25: https://developers.arcgis.com/javascript/latest/4.25/#search-performance-improvements
This AGOL blog also gives additional insights into why this change may have occurred: https://www.esri.com/arcgis-blog/products/arcgis-online/mapping/searching-for-features-in-maps-and-a...
Idea thread found here.
Solution:
// Configure only your parcel layer sources
// Custom getSuggestions bypasses Esri's 5.0 prefix-only behavior
// and restores the old LIKE '%value%' contains search
quickSearch.sources = [
{
layer: parcelLayer,
searchFields: ["PartyName_1"],
displayField: "PartyName_1",
name: "Search by Owner",
outFields: ["PartyName_1"],
exactMatch: false,
suggestionsEnabled: true,
getSuggestions: (params) => {
const query = parcelLayer.createQuery();
query.where = `PartyName_1 LIKE '%${params.suggestTerm.replace(/'/g, "''")}%'`;
query.outFields = ["PartyName_1"];
query.returnGeometry = false;
query.returnDistinctValues = true;
query.orderByFields = ["PartyName_1"];
query.num = 6;
return parcelLayer.queryFeatures(query).then(results => {
return results.features.map(f => ({
key: f.attributes.PartyName_1,
text: f.attributes.PartyName_1,
sourceIndex: params.sourceIndex
}));
});
}
},
{
layer: parcelLayer,
searchFields: ["PropertyAddress"],
displayField: "PropertyAddress",
name: "Search by Address",
outFields: ["PropertyAddress"],
exactMatch: false,
suggestionsEnabled: true,
getSuggestions: (params) => {
const query = parcelLayer.createQuery();
query.where = `PropertyAddress LIKE '%${params.suggestTerm.replace(/'/g, "''")}%'`;
query.outFields = ["PropertyAddress"];
query.returnGeometry = false;
query.returnDistinctValues = true;
query.orderByFields = ["PropertyAddress"];
query.num = 6;
return parcelLayer.queryFeatures(query).then(results => {
return results.features.map(f => ({
key: f.attributes.PropertyAddress,
text: f.attributes.PropertyAddress,
sourceIndex: params.sourceIndex
}));
});
}
},
{
layer: parcelLayer,
searchFields: ["QuickRefID_1"],
displayField: "QuickRefID_1",
name: "Search by Quick Reference ID",
outFields: ["QuickRefID_1"],
exactMatch: false,
suggestionsEnabled: true,
getSuggestions: (params) => {
const query = parcelLayer.createQuery();
query.where = `QuickRefID_1 LIKE '%${params.suggestTerm.replace(/'/g, "''")}%'`;
query.outFields = ["QuickRefID_1"];
query.returnGeometry = false;
query.returnDistinctValues = true;
query.orderByFields = ["QuickRefID_1"];
query.num = 6;
return parcelLayer.queryFeatures(query).then(results => {
return results.features.map(f => ({
key: f.attributes.QuickRefID_1,
text: f.attributes.QuickRefID_1,
sourceIndex: params.sourceIndex
}));
});
}
}
];
Following this. Not sure if this is related, but we have noticed few differences as well. It seems that Search in ExP works differently to Search in Map SDK. For example, when I search "Nora", below are the results I have obtained from Search widget in Map SDK and Search widget from ExP Builder.
Search result for "Nora" in Map SDK Search widget - https://imgur.com/a/XWumpC0
Search result for "Nora" in ExP Search widget - https://imgur.com/a/sPxLVy6
The results seem different regardless of the setting. In the ExP builder, we can see the results include the search term being in the middle of the result whereas in the Map SDK version, it always displays the start's with results.
@MatthewDriscoll This is the expected behavior. The change was introduced in 4.25: https://developers.arcgis.com/javascript/latest/4.25/#search-performance-improvements
This AGOL blog also gives additional insights into why this change may have occurred: https://www.esri.com/arcgis-blog/products/arcgis-online/mapping/searching-for-features-in-maps-and-a...
Is there a known fix or do I go create my own? Seems like it makes this component pretty useless for anything other then location. @NZGIS this is directly related to your problem, it is prefix only now.
I am not aware of any ways to adjust this through the API. However, adding "%" to the front of your search term should do the trick.
@NZGIS , This feature exists in ExB's Search Widget, but only as a search result rather than a search suggestion. Let me know if there are any questions on this.
Idea thread found here.
Solution:
// Configure only your parcel layer sources
// Custom getSuggestions bypasses Esri's 5.0 prefix-only behavior
// and restores the old LIKE '%value%' contains search
quickSearch.sources = [
{
layer: parcelLayer,
searchFields: ["PartyName_1"],
displayField: "PartyName_1",
name: "Search by Owner",
outFields: ["PartyName_1"],
exactMatch: false,
suggestionsEnabled: true,
getSuggestions: (params) => {
const query = parcelLayer.createQuery();
query.where = `PartyName_1 LIKE '%${params.suggestTerm.replace(/'/g, "''")}%'`;
query.outFields = ["PartyName_1"];
query.returnGeometry = false;
query.returnDistinctValues = true;
query.orderByFields = ["PartyName_1"];
query.num = 6;
return parcelLayer.queryFeatures(query).then(results => {
return results.features.map(f => ({
key: f.attributes.PartyName_1,
text: f.attributes.PartyName_1,
sourceIndex: params.sourceIndex
}));
});
}
},
{
layer: parcelLayer,
searchFields: ["PropertyAddress"],
displayField: "PropertyAddress",
name: "Search by Address",
outFields: ["PropertyAddress"],
exactMatch: false,
suggestionsEnabled: true,
getSuggestions: (params) => {
const query = parcelLayer.createQuery();
query.where = `PropertyAddress LIKE '%${params.suggestTerm.replace(/'/g, "''")}%'`;
query.outFields = ["PropertyAddress"];
query.returnGeometry = false;
query.returnDistinctValues = true;
query.orderByFields = ["PropertyAddress"];
query.num = 6;
return parcelLayer.queryFeatures(query).then(results => {
return results.features.map(f => ({
key: f.attributes.PropertyAddress,
text: f.attributes.PropertyAddress,
sourceIndex: params.sourceIndex
}));
});
}
},
{
layer: parcelLayer,
searchFields: ["QuickRefID_1"],
displayField: "QuickRefID_1",
name: "Search by Quick Reference ID",
outFields: ["QuickRefID_1"],
exactMatch: false,
suggestionsEnabled: true,
getSuggestions: (params) => {
const query = parcelLayer.createQuery();
query.where = `QuickRefID_1 LIKE '%${params.suggestTerm.replace(/'/g, "''")}%'`;
query.outFields = ["QuickRefID_1"];
query.returnGeometry = false;
query.returnDistinctValues = true;
query.orderByFields = ["QuickRefID_1"];
query.num = 6;
return parcelLayer.queryFeatures(query).then(results => {
return results.features.map(f => ({
key: f.attributes.QuickRefID_1,
text: f.attributes.QuickRefID_1,
sourceIndex: params.sourceIndex
}));
});
}
}
];
this works very well.
Instead of using hard coded num 6 I guess you can read the effective maxSuggestions from the params.
query.num = params.maxSuggestions;
We also am using multiple fields as search fields I need to map them for the whereclause.
Additionally I put them as outFields and orderByFields.
Here is my code:
/**
* Creates a suggestion callback that restores contains-style matching for all configured search fields.
* @Param layer Feature layer queried for suggestions
* @Param searchFields Fields that are searched using LIKE '%term%'
* @Param displayField Preferred field used for sorting and suggestion display
*/
private createContainsSuggestions(
layer: FeatureLayer,
searchFields: string[],
displayField?: string | null
): NonNullable<LayerSearchSourceProperties['getSuggestions']> {
return async (params) => {
const suggestTerm = params.suggestTerm?.trim();
if (!suggestTerm) {
return [];
}
const escapedTerm = this.escapeSqlStringLiteral(suggestTerm);
const query = layer.createQuery();
query.where = searchFields
.map(searchField => `${searchField} LIKE '%${escapedTerm}%'`)
.join(' OR ');
query.outFields = searchFields;
query.returnGeometry = false;
query.returnDistinctValues = true;
query.orderByFields = searchFields;
if (typeof params.maxSuggestions === 'number') {
query.num = params.maxSuggestions;
}
const result = await layer.queryFeatures(query);
return result.features.flatMap(feature => {
const textValue = displayField ? feature.attributes?.[displayField] : undefined;
if (!textValue) {
return [];
}
return [{
key: textValue,
text: textValue,
sourceIndex: params.sourceIndex
}];
});
};
}
/**
* Escapes single quotes for safe use in SQL string literals.
* @Param value Raw user-provided search value
*/
private escapeSqlStringLiteral(value: string): string {
return value.replace(/'/g, "''");
}
this solution only works for the suggestionList right?
How do we enable it for the actual search as well after hitting enter?
This is, what is sent to server:
query?f=pbf
&fullText=[{"onFields":["search_names"],"searchTerm":"Defi","searchType":"prefix"}]
&maxAllowableOffset=1
&resultRecordCount=6
&where=1=1
&outFields=*
&outSR=102100
&spatialRel=esriSpatialRelIntersects
How can we apply a "like" here?
I solved that by creating an interceptor looking for searchTerm and replacing it with a where clause.
This works for the actual search and the suggestionList as well.
I also added a LOWER to the like clause to support case insensitivity.
interface FullTextSearchExpression {
onFields?: string[];
searchTerm?: string;
searchOperator?: 'and' | 'or';
}
interface MutableQuery extends Record<string, any> {
where?: unknown;
fullText?: unknown;
}
/**
* Registers an ArcGIS query interceptor that maps fullText search terms to SQL LIKE where clauses.
* @return void
*/
private interceptQueryRequests(): void {
const queryInterceptor: RequestInterceptor = {
urls: '/\\/query\\/?(?:\\?.*)?$/i',
before: (params) => {
const query = params.requestOptions.query;
if (!query || query instanceof URLSearchParams || typeof query !== 'object') {
return;
}
const mutableQuery = query as MutableQuery;
const fullTextExpressions = this.parseFullTextExpressions(mutableQuery.fullText);
if (!fullTextExpressions.length) {
return;
}
const fullTextWhereClause = this.createFullTextWhereClause(fullTextExpressions);
if (!fullTextWhereClause) {
return;
}
mutableQuery.where = this.mergeWhereClauses(mutableQuery.where, fullTextWhereClause);
delete mutableQuery.fullText;
}
};
esriConfig.request.interceptors?.push(queryInterceptor);
}
/**
* Parses fullText request payload to a list of search expressions.
* @Param fullText Full text payload from query parameters
* @return Parsed expressions or empty array when payload is invalid
*/
private parseFullTextExpressions(fullText: unknown): FullTextSearchExpression[] {
if (!fullText) {
return [];
}
if (Array.isArray(fullText)) {
return fullText.filter((item): item is FullTextSearchExpression => typeof item === 'object' && item !== null);
}
if (typeof fullText === 'string') {
try {
const parsed = JSON.parse(fullText);
return Array.isArray(parsed)
? parsed.filter((item): item is FullTextSearchExpression => typeof item === 'object' && item !== null)
: [];
} catch {
return [];
}
}
return [];
}
/**
* Builds SQL where clauses from fullText expressions using LIKE with contains wildcards.
* @Param expressions Full text expressions to transform
* @return SQL where fragment or undefined if no valid conditions can be generated
*/
private createFullTextWhereClause(expressions: FullTextSearchExpression[]): string | undefined {
const clauses: string[] = [];
for (let index = 0; index < expressions.length; index++) {
const expression = expressions[index];
const fields = expression.onFields?.filter(field => field && field !== '*') ?? [];
const rawTerm = expression.searchTerm?.trim();
if (!fields.length || !rawTerm) {
continue;
}
const termWithWildcards = this.ensureContainsWildcard(rawTerm);
const escapedTerm = this.escapeSqlStringLiteral(termWithWildcards);
const fieldClause = fields
.map(field => `LOWER(${field}) LIKE LOWER('${escapedTerm}')`)
.join(' OR ');
if (!fieldClause) {
continue;
}
if (!clauses.length) {
clauses.push(fieldClause);
continue;
}
const previousOperator = expressions[index - 1]?.searchOperator?.toUpperCase() === 'OR' ? 'OR' : 'AND';
clauses.push(`${previousOperator} ${fieldClause}`);
}
if (!clauses.length) {
return undefined;
}
return clauses.join(' ');
}
/**
* Ensures a term uses contains-style wildcards unless they are already present.
* @Param term Search term
* @return Term wrapped with % wildcard markers
*/
private ensureContainsWildcard(term: string): string {
let wildcardTerm = term;
if (!wildcardTerm.startsWith('%')) {
wildcardTerm = `%${wildcardTerm}`;
}
if (!wildcardTerm.endsWith('%')) {
wildcardTerm = `${wildcardTerm}%`;
}
return wildcardTerm;
}
/**
* Merges an existing where clause with an additional one.
* @Param existingWhere Existing where value
* @Param additionalWhere Additional where clause to append
* @return Combined where clause
*/
private mergeWhereClauses(existingWhere: unknown, additionalWhere: string): string {
const normalizedExisting = typeof existingWhere === 'string' ? existingWhere.trim() : '';
if (!normalizedExisting) {
return additionalWhere;
}
return `${normalizedExisting} AND ${additionalWhere}`;
}