A new Power App search
Announced in early November, the new search experience in model-driven Power Apps is a game-changer feature. Not only the design of the search is changed, but the technology behind it evolved too. Based on the latest improvements made on the Relevance search, these new search capabilities finally give Power Apps the base for a powerful search engine.
As explained here, the Relevance search delivers fast and comprehensive search results across multiple entities, in a single list, sorted by ... relevance! In this article, we will take a closer look at the update and new capabilities of the Relevance search. We will investigate what are the new features, how developers can leverage them, and how a custom made search engine can be easily built upon.
Before we dive in, it's worth mentioning that the Relevance search results will always depend upon the logged user. All results are based on the environment’s configured entity data and what the user has permission to access.
Quick Overview
Announced mid-October 2020, the new Relevance search's capabilities are divided into three types of searches:
- Search: The simple search, that returns results based on a query.
- Suggestions: Based on the first characters written, provides suggestions to records.
- Autocomplete: Based on the first characters written, provides suggestions to the search query input.
But the real advantage of the new capabilities is the customization available for all three types of search. Techniques from the Natural Language Processing (NLP) and Artificial Intelligence (AI) world are now accessible directly for the Relevance search! Be it Fuzzy search, Term boosting or proximity search, developers have now the ability to retrieved much finer results from a search.
New intelligent capabilities
With this new release of the Relevance search API, three NLP features that are now the standard in a search engine are finally available. All features below can be used when making a query with the Lucene syntax. As detailed in another blog post, the Relevance search is based on an Azure Search Index. The Azure Search Index offers a simple query parser by default, but it can also use a Lucene Query parser. This more advanced way of querying data enables us to formulates specialized and advanced query definitions.
The Lucene parser supports complex query constructs, such as fuzzy search, infix and suffix wildcard search, proximity search, term boosting, and regular expression search. Obviously, as the queries are more complex, the search execution time will take a bit longer.
Fuzzy search
A fuzzy search finds matches in terms that have a similar construction, enabling the query to return correct results even in the case of typos or misspelled terms in the inputs. Within the Relevance search API, fuzzy search queries can support misspellings by up to 2 characters. This means that the term "Uniersty" can return the term "University", because there is a difference of 2 characters between the two terms ("University")
Behind the hood, a search engine with fuzzy search will build a graph of similarly composed terms. For a term like "universe", the graph might have "unvers", "universe", "univers", or "inverse". All the words in the graph are words obtained from "universe" and where two or fewer edits have been applied, where an edit is an inserted, deleted, substituted, or transposed character.
The last word in the list is interesting, because "universe" and "inverse" are indeed close one to the other in terms of Damerau-Levenshtein distance, but they are not close in terms of semantics. This is one of the limitations of such fuzzy search, that does not take into account the semantics of the terms. Maybe on a future version!
Note that on the Lucene syntax, it's possible to specify the distance. For example, "blue~1” will match “glue”, “blues”, but it will not match "glues".
Fuzzy search can only be applied to terms, not phrases.
Term boosting
Term boosting refers to ranking a record higher if it contains the boosted term, relative to records that do not contain the term. This differs from scoring profiles in that scoring profiles boost certain fields, rather than specific terms.
For example, the query "Rock^2 electronic” will return results where the matches of “rock” are more important than matches to “electronic”.
To boost a term use the caret, "^", a symbol with a boost factor (a number) at the end of the term you are searching. The higher the boost factor, the more relevant the term will be relative to other search terms. By default, the boost factor is 1. Although the boost factor must be positive, it can be less than 1 (for example, 0.20).
Proximity search
Proximity searches are used to find terms that are near each other in a document. The number of words separating the two terms must be explicitly specified in the query.
For example, “airport hotel”~5 returns results where “airport” and “hotel” are within 5 words of each other, thus boosting the chances of finding a hotel located close to an airport.
Regular expression
The Lucene syntax also supports the use of regular expression (Regex). Even if this functionality is not "intelligent" in terms of AI, it's a very important capability, because almost all developers know what a regular expression is and how to use it! Again, that's a very powerful tool to have in a search engine.
Advanced search capabilities
All capabilities mentioned above can only be used in a Relevance search using the Lucene query parser. But the simple parser is not useless! Below are some capabilities offered by default search that will enable queries to go deeper into the data.
Note that the Lucene query parser also supports those functionalities - exception made for the exact match capabilities.
Boolean operators
Very simple to use and understand. There are three boolean operators that can be added to a query:
- AND: denoted by "+"
- OR: denoted by "|"
- NOT: denoted by "-"
Let's make an example. If a query with the search term "danny" return the contact "Danny Alves" and another query with "dani" returns the contact "Dani Rodrigues", the query with "danny | dani" will return both contacts!
Precedence operators
A search term "hotel+(wifi | luxury)" will search for results containing the term “hotel” and either “wifi” or “luxury” (or both).
Very useful when combined with boolean operators!
Wildcards
The "*" can be added to the search term and the query will match everything that contains the characters before the "*". For example, "Alp*" searches for "alpine".
Note that the Lucene query syntax enables us to have leading wildcards. For example, "*ine" searches for "alpine".
Exact matches
Enclosed a search term within quotation marks “ “. The Lucene query syntax doesn't support exact matches.
Hello World Query
The new Relevance search capabilities can be used via its API. And before that, Relevance search must obviously be enabled in the environment, as described here. Once enabled, a Relevance search query can be made via a simple HTTP POST request.
In this article, we will use Postman to build and make the HTTP calls. The article "Tutorial on how to connect step by step to D365 with Postman" explains in detail how to connect to your environments from Postman.
Once the connection has been set up and the WhoAmI request returned successfully, we can make a simple search request.
For this example, we will query the environment to look for a contact named "Danny Rodrigues Alves". The HTTP endpoint for this search is "/api/search/v1.0/query" and the content sent is very simple:
POST [Organization URI]/api/search/v1.0/query
{
"search": "danny"
}
The result will be a JSON object with an array "value": all records that match the query will be in this array. In the below screenshot, only one record matches the query. The JSON element @search.entityname specifies the entity of the record retrieved and the element @search.objectid it's GUID.
Search
Input
As seen above, the most important feature of the Relevance Search API is to... search for data! With the /query endpoint, the HTTP post request only needs to have a "search" element in its data. the search parameter value contains the term to be searched for and has a 100-character limit.
To use the new features offered by the API, additional query parameters can and must be added to the HTTP POST call. There is no point to detail here all input parameters that can be added to the request because Microsoft's documentation is pretty clear for that part.
Let's make a simple example: a query with a fuzzy search.
POST [Organization URI]/api/search/v1.0/query
{
"search": "danni~",
"searchtype": "full",
"returntotalrecordcount":true
}
It will return all records that have - in their Find columns fields - the term "danni" or any term up to a Damerau-Levenshtein distance of 2. The raw JSON returned is:
{
"value": [
{
"@search.score": 3.7685628,
"@search.highlights": {
"firstname": [
"{crmhit}Danni{/crmhit}"
],
"fullname": [
"{crmhit}Danni{/crmhit} Rodriguez"
]
},
"@search.entityname": "contact",
"@search.objectid": "c8527da4-e21d-eb11-a813-000d3ab85224",
"key": "c8527da4-e21d-eb11-a813-000d3ab85224contact2",
"ownerid": "4a390a3c-df19-eb11-a812-000d3ab85fc1",
"owneridname": "MOD Administrator",
"@search.ownerid.logicalname": "systemuser",
"owningbusinessunit": "0158d183-b119-eb11-a812-000d3ab85fc1",
"owningbusinessunitname": "",
"@search.owningbusinessunit.logicalname": "businessunit",
"sharedtoprincipalid": [],
"@search.objecttypecode": 2,
"fullname": "Danni Rodriguez",
"versionnumber": 3552556,
"statecode@stringcollection": [
"Active"
],
"statecode": 0,
"statuscode@stringcollection": [
"Active"
],
"statuscode": 1,
"entityimage_url": null,
"lastsyncdate": "/Date(1604855416699)/",
"createdon": "11/3/2020 3:41 PM",
"modifiedon": "11/3/2020 3:41 PM",
"documentbody": null,
"body": null,
"filebody": null,
"emailaddress1": null,
"address1_city": null,
"telephone1": null,
"address1_telephone1": null,
"parentcustomerid": null,
"parentcustomeridname": null,
"donotbulkpostalmail": false,
"donotbulkemail": false,
"donotpostalmail": false,
"donotfax": false,
"donotemail": false,
"department": null,
"territorycode": [
"Default Value"
],
"familystatuscode": [],
"paymenttermscode": []
},
{
"@search.score": 2.956873,
"@search.highlights": {
"firstname": [
"{crmhit}Janna{/crmhit}"
],
"emailaddress1": [
"{crmhit}janna{/crmhit}_santana@fabrikam.com"
],
"fullname": [
"{crmhit}Janna{/crmhit} Santana"
]
},
"@search.entityname": "contact",
"@search.objectid": "05a17064-1ae7-e611-80f4-e0071b661f01",
"key": "05a17064-1ae7-e611-80f4-e0071b661f01contact2",
"ownerid": "f35ed183-b119-eb11-a812-000d3ab85fc1",
"owneridname": "--- ---",
"@search.ownerid.logicalname": "systemuser",
"owningbusinessunit": "0158d183-b119-eb11-a812-000d3ab85fc1",
"owningbusinessunitname": "",
"@search.owningbusinessunit.logicalname": "businessunit",
"sharedtoprincipalid": [],
"@search.objecttypecode": 2,
"fullname": "Janna Santana",
"versionnumber": 2311278,
"statecode@stringcollection": [
"Active"
],
"statecode": 0,
"statuscode@stringcollection": [
"Active"
],
"statuscode": 1,
"entityimage_url": "/Image/download.aspx?Entity=contact&Attribute=entityimage&Id=05a17064-1ae7-e611-80f4-e0071b661f01&Timestamp=637395711725487934",
"lastsyncdate": "/Date(1604855416558)/",
"createdon": "10/29/2020 1:26 PM",
"modifiedon": "10/29/2020 1:26 PM",
"documentbody": null,
"body": null,
"filebody": null,
"emailaddress1": "janna_santana@fabrikam.com",
"address1_city": null,
"telephone1": null,
"address1_telephone1": null,
"parentcustomerid": null,
"parentcustomeridname": null,
"donotbulkpostalmail": false,
"donotbulkemail": false,
"donotpostalmail": false,
"donotfax": false,
"donotemail": false,
"department": null,
"territorycode": [
"Default Value"
],
"familystatuscode": [],
"paymenttermscode": []
},
{
"@search.score": 2.956873,
"@search.highlights": {
"firstname": [
"{crmhit}Dianna{/crmhit}"
],
"emailaddress1": [
"{crmhit}dianna{/crmhit}_woodward@fabrikam.com"
],
"fullname": [
"{crmhit}Dianna{/crmhit} Woodward"
]
},
"@search.entityname": "contact",
"@search.objectid": "23a17064-1ae7-e611-80f4-e0071b661f01",
"key": "23a17064-1ae7-e611-80f4-e0071b661f01contact2",
"ownerid": "f35ed183-b119-eb11-a812-000d3ab85fc1",
"owneridname": "--- ---",
"@search.ownerid.logicalname": "systemuser",
"owningbusinessunit": "0158d183-b119-eb11-a812-000d3ab85fc1",
"owningbusinessunitname": "",
"@search.owningbusinessunit.logicalname": "businessunit",
"sharedtoprincipalid": [],
"@search.objecttypecode": 2,
"fullname": "Dianna Woodward",
"versionnumber": 2311371,
"statecode@stringcollection": [
"Active"
],
"statecode": 0,
"statuscode@stringcollection": [
"Active"
],
"statuscode": 1,
"entityimage_url": "/Image/download.aspx?Entity=contact&Attribute=entityimage&Id=23a17064-1ae7-e611-80f4-e0071b661f01&Timestamp=637395711733769247",
"lastsyncdate": "/Date(1604855416589)/",
"createdon": "10/29/2020 1:26 PM",
"modifiedon": "10/29/2020 1:26 PM",
"documentbody": null,
"body": null,
"filebody": null,
"emailaddress1": "dianna_woodward@fabrikam.com",
"address1_city": null,
"telephone1": null,
"address1_telephone1": null,
"parentcustomerid": null,
"parentcustomeridname": null,
"donotbulkpostalmail": false,
"donotbulkemail": false,
"donotpostalmail": false,
"donotfax": false,
"donotemail": false,
"department": null,
"territorycode": [
"Default Value"
],
"familystatuscode": [],
"paymenttermscode": []
},
{
"@search.score": 2.8264222,
"@search.highlights": {
"firstname": [
"{crmhit}Dani{/crmhit}"
],
"fullname": [
"{crmhit}Dani{/crmhit} Rodrigues"
]
},
"@search.entityname": "contact",
"@search.objectid": "61dada9b-e21d-eb11-a813-000d3ab85224",
"key": "61dada9b-e21d-eb11-a813-000d3ab85224contact2",
"ownerid": "4a390a3c-df19-eb11-a812-000d3ab85fc1",
"owneridname": "MOD Administrator",
"@search.ownerid.logicalname": "systemuser",
"owningbusinessunit": "0158d183-b119-eb11-a812-000d3ab85fc1",
"owningbusinessunitname": "",
"@search.owningbusinessunit.logicalname": "businessunit",
"sharedtoprincipalid": [],
"@search.objecttypecode": 2,
"fullname": "Dani Rodrigues",
"versionnumber": 3552525,
"statecode@stringcollection": [
"Active"
],
"statecode": 0,
"statuscode@stringcollection": [
"Active"
],
"statuscode": 1,
"entityimage_url": null,
"lastsyncdate": "/Date(1604855416699)/",
"createdon": "11/3/2020 3:41 PM",
"modifiedon": "11/3/2020 3:41 PM",
"documentbody": null,
"body": null,
"filebody": null,
"emailaddress1": null,
"address1_city": null,
"telephone1": null,
"address1_telephone1": null,
"parentcustomerid": null,
"parentcustomeridname": null,
"donotbulkpostalmail": false,
"donotbulkemail": false,
"donotpostalmail": false,
"donotfax": false,
"donotemail": false,
"department": null,
"territorycode": [
"Default Value"
],
"familystatuscode": [],
"paymenttermscode": []
},
{
"@search.score": 2.782939,
"@search.highlights": {
"firstname": [
"{crmhit}Danny{/crmhit}"
],
"fullname": [
"{crmhit}Danny{/crmhit} Alves Rodrigues"
]
},
"@search.entityname": "contact",
"@search.objectid": "48ff0ab2-e21d-eb11-a813-000d3ab85224",
"key": "48ff0ab2-e21d-eb11-a813-000d3ab85224contact2",
"ownerid": "4a390a3c-df19-eb11-a812-000d3ab85fc1",
"owneridname": "MOD Administrator",
"@search.ownerid.logicalname": "systemuser",
"owningbusinessunit": "0158d183-b119-eb11-a812-000d3ab85fc1",
"owningbusinessunitname": "",
"@search.owningbusinessunit.logicalname": "businessunit",
"sharedtoprincipalid": [],
"@search.objecttypecode": 2,
"fullname": "Danny Alves Rodrigues",
"versionnumber": 3903781,
"statecode@stringcollection": [
"Active"
],
"statecode": 0,
"statuscode@stringcollection": [
"Active"
],
"statuscode": 1,
"entityimage_url": null,
"lastsyncdate": "/Date(1604855416699)/",
"createdon": "11/3/2020 3:41 PM",
"modifiedon": "11/8/2020 6:09 PM",
"documentbody": null,
"body": null,
"filebody": null,
"emailaddress1": null,
"address1_city": null,
"telephone1": null,
"address1_telephone1": null,
"parentcustomerid": null,
"parentcustomeridname": null,
"donotbulkpostalmail": false,
"donotbulkemail": false,
"donotpostalmail": false,
"donotfax": false,
"donotemail": false,
"department": null,
"territorycode": [
"Default Value"
],
"familystatuscode": [],
"paymenttermscode": [
"Net 30"
]
},
{
"@search.score": 2.4640603,
"@search.highlights": {
"firstname": [
"{crmhit}Dawn{/crmhit}"
],
"emailaddress1": [
"{crmhit}dawn{/crmhit}_phelps@fabrikam.com"
],
"fullname": [
"{crmhit}Dawn{/crmhit} Phelps"
]
},
"@search.entityname": "contact",
"@search.objectid": "17a17064-1ae7-e611-80f4-e0071b661f01",
"key": "17a17064-1ae7-e611-80f4-e0071b661f01contact2",
"ownerid": "f35ed183-b119-eb11-a812-000d3ab85fc1",
"owneridname": "--- ---",
"@search.ownerid.logicalname": "systemuser",
"owningbusinessunit": "0158d183-b119-eb11-a812-000d3ab85fc1",
"owningbusinessunitname": "",
"@search.owningbusinessunit.logicalname": "businessunit",
"sharedtoprincipalid": [],
"@search.objecttypecode": 2,
"fullname": "Dawn Phelps",
"versionnumber": 2311334,
"statecode@stringcollection": [
"Active"
],
"statecode": 0,
"statuscode@stringcollection": [
"Active"
],
"statuscode": 1,
"entityimage_url": "/Image/download.aspx?Entity=contact&Attribute=entityimage&Id=17a17064-1ae7-e611-80f4-e0071b661f01&Timestamp=637395711730174992",
"lastsyncdate": "/Date(1604855416574)/",
"createdon": "10/29/2020 1:26 PM",
"modifiedon": "10/29/2020 1:26 PM",
"documentbody": null,
"body": null,
"filebody": null,
"emailaddress1": "dawn_phelps@fabrikam.com",
"address1_city": null,
"telephone1": null,
"address1_telephone1": null,
"parentcustomerid": null,
"parentcustomeridname": null,
"donotbulkpostalmail": false,
"donotbulkemail": false,
"donotpostalmail": false,
"donotfax": false,
"donotemail": false,
"department": null,
"territorycode": [
"Default Value"
],
"familystatuscode": [],
"paymenttermscode": []
},
{
"@search.score": 2.261138,
"@search.highlights": {
"firstname": [
"{crmhit}Nana{/crmhit}"
],
"fullname": [
"{crmhit}Nana{/crmhit} Bule"
]
},
"@search.entityname": "contact",
"@search.objectid": "55a1e5b9-88df-e311-b8e5-6c3be5a8b200",
"key": "55a1e5b9-88df-e311-b8e5-6c3be5a8b200contact2",
"ownerid": "92390a3c-df19-eb11-a812-000d3ab85fc1",
"owneridname": "Veronica Quek (Sample Data)",
"@search.ownerid.logicalname": "systemuser",
"owningbusinessunit": "0158d183-b119-eb11-a812-000d3ab85fc1",
"owningbusinessunitname": "",
"@search.owningbusinessunit.logicalname": "businessunit",
"sharedtoprincipalid": [],
"@search.objecttypecode": 2,
"fullname": "Nana Bule",
"versionnumber": 2303550,
"statecode@stringcollection": [
"Active"
],
"statecode": 0,
"statuscode@stringcollection": [
"Active"
],
"statuscode": 1,
"entityimage_url": null,
"lastsyncdate": "/Date(1604855416308)/",
"createdon": "1/20/2017 11:39 PM",
"modifiedon": "10/29/2020 1:18 PM",
"documentbody": null,
"body": null,
"filebody": null,
"emailaddress1": "info@fineartschool.net",
"address1_city": "Mississaugua",
"telephone1": "012-156-8770",
"address1_telephone1": null,
"parentcustomerid": "c6a19cdd-88df-e311-b8e5-6c3be5a8b200",
"parentcustomeridname": "School of Fine Art",
"@search.parentcustomerid.logicalname": "account",
"donotbulkpostalmail": false,
"donotbulkemail": false,
"donotpostalmail": false,
"donotfax": false,
"donotemail": false,
"department": null,
"territorycode": [
"Default Value"
],
"familystatuscode": [],
"paymenttermscode": []
},
{
"@search.score": 2.261138,
"@search.highlights": {
"fullname": [
"Mack {crmhit}Fannin{/crmhit}"
],
"lastname": [
"{crmhit}Fannin{/crmhit}"
]
},
"@search.entityname": "contact",
"@search.objectid": "bda07064-1ae7-e611-80f4-e0071b661f01",
"key": "bda07064-1ae7-e611-80f4-e0071b661f01contact2",
"ownerid": "f35ed183-b119-eb11-a812-000d3ab85fc1",
"owneridname": "--- ---",
"@search.ownerid.logicalname": "systemuser",
"owningbusinessunit": "0158d183-b119-eb11-a812-000d3ab85fc1",
"owningbusinessunitname": "",
"@search.owningbusinessunit.logicalname": "businessunit",
"sharedtoprincipalid": [],
"@search.objecttypecode": 2,
"fullname": "Mack Fannin",
"versionnumber": 2311095,
"statecode@stringcollection": [
"Active"
],
"statecode": 0,
"statuscode@stringcollection": [
"Active"
],
"statuscode": 1,
"entityimage_url": null,
"lastsyncdate": "/Date(1604855416480)/",
"createdon": "10/29/2020 1:26 PM",
"modifiedon": "10/29/2020 1:26 PM",
"documentbody": null,
"body": null,
"filebody": null,
"emailaddress1": null,
"address1_city": null,
"telephone1": "206-555-8595",
"address1_telephone1": null,
"parentcustomerid": "f76b3f4b-1be7-e611-8101-e0071b6af231",
"parentcustomeridname": "Litware, Inc.",
"@search.parentcustomerid.logicalname": "account",
"donotbulkpostalmail": false,
"donotbulkemail": false,
"donotpostalmail": false,
"donotfax": false,
"donotemail": false,
"department": null,
"territorycode": [
"Default Value"
],
"familystatuscode": [],
"paymenttermscode": []
},
{
"@search.score": 1.8842814,
"@search.highlights": {
"firstname": [
"{crmhit}Dany{/crmhit}"
],
"fullname": [
"{crmhit}Dany{/crmhit} Rordiuge"
]
},
"@search.entityname": "contact",
"@search.objectid": "803782aa-e21d-eb11-a813-000d3ab85224",
"key": "803782aa-e21d-eb11-a813-000d3ab85224contact2",
"ownerid": "4a390a3c-df19-eb11-a812-000d3ab85fc1",
"owneridname": "MOD Administrator",
"@search.ownerid.logicalname": "systemuser",
"owningbusinessunit": "0158d183-b119-eb11-a812-000d3ab85fc1",
"owningbusinessunitname": "",
"@search.owningbusinessunit.logicalname": "businessunit",
"sharedtoprincipalid": [],
"@search.objecttypecode": 2,
"fullname": "Dany Rordiuge",
"versionnumber": 3552566,
"statecode@stringcollection": [
"Active"
],
"statecode": 0,
"statuscode@stringcollection": [
"Active"
],
"statuscode": 1,
"entityimage_url": null,
"lastsyncdate": "/Date(1604855416699)/",
"createdon": "11/3/2020 3:41 PM",
"modifiedon": "11/3/2020 3:41 PM",
"documentbody": null,
"body": null,
"filebody": null,
"emailaddress1": null,
"address1_city": null,
"telephone1": null,
"address1_telephone1": null,
"parentcustomerid": null,
"parentcustomeridname": null,
"donotbulkpostalmail": false,
"donotbulkemail": false,
"donotpostalmail": false,
"donotfax": false,
"donotemail": false,
"department": null,
"territorycode": [
"Default Value"
],
"familystatuscode": [],
"paymenttermscode": []
}
],
"facets": {},
"totalrecordcount": 9
}
In the following table, we highlight the main elements returned per record:
Fullname | @search.score | @search.highlights #1 | @search.highlights #2 | @search.highlights #3 |
---|---|---|---|---|
Danni Rodriguez | 3.7685628 | {crmhit}Danni{/crmhit} | {crmhit}Danni{/crmhit} Rodriguez | |
Janna Santana | 2.956873 | {crmhit}Janna{/crmhit} | {crmhit}janna{/crmhit}_santana@fabrikam.com | {crmhit}Janna{/crmhit} Santana |
Dianna Woodward | 2.956873 | {crmhit}Dianna{/crmhit} | {crmhit}dianna{/crmhit}_woodward@fabrikam.com | {crmhit}Dianna{/crmhit} Woodward |
Dani Rodrigues | 2.8264222 | {crmhit}Dani{/crmhit} | {crmhit}Dani{/crmhit} Rodrigues | |
Danny Alves Rodrigues | 2.782939 | {crmhit}Danny{/crmhit} | {crmhit}Danny{/crmhit} Alves Rodrigues | |
Dawn Phelps | 2.4640603 | {crmhit}Dawn{/crmhit} | {crmhit}dawn{/crmhit}_phelps@fabrikam.com | {crmhit}Dawn{/crmhit} Phelps |
Nana Bule | 2.261138 | {crmhit}Nana{/crmhit} | crmhit}Nana{/crmhit} Bule | |
Mack Fannin | 2.261138 | Mack {crmhit}Fannin{/crmhit} | {crmhit}Fannin{/crmhit} | |
Dany Rordiuge | 1.8842814 | {crmhit}Dany{/crmhit} | {crmhit}Dany{/crmhit} Rordiuge |
There are a lot of things to discuss in this table, but the most important is that a distance of two can return a lot of results! Indeed, how imagine that "danni" and "nana" were so close? Or that by searching for "danni" - that is a firstname - the contact "Mack Fannin" is also matched? The score is also very important to present the results to the users, and what about the @search.highlights elements? Not all records have the same number of elements, but it gives an easy way to directly understand how the search algorithm works and why such record has been matched.
Another interesting input and output element is the facets element. Microsoft's documentation contains an excellent example of such a request.
Output
Regarding the result returned by the API, it will always contain three elements - if the HTTP call was successful and returned the HTTP status 200.
- value: the list of all records that match the query. By default, up to 50 results are returned.
- For all records returned, there will always have the "@search.score" and "@search.highlights" elements. The score can be used to rank results and the highlights can be used to display in bold why the record has been matched. That's exactly what the default relevance search in Dynamics 365 does.
- For all records returned, some elements will be specific to the type of entity. For example, a "contact" record will have a "fullname" object were a "case" record will not.
- The elements returned per entity are defined by the Quick Find view.
- totalrecordcount: The number of results in the value array. A value of -1 is returned if the input parameter "
returntotalrecordcount"
was set to false - it is by default. - facets: The facet results that can then be used to drill into the data.
For examples, with the following HTTP POST request:
POST [Organization URI]/api/search/v1.0/query
{
"search": "danny | dani",
"facets" : ["@search.entityname,count:100"],
"returntotalrecordcount":true
}
The result is:
{
"value": [
{
"@search.score": 0.68580884,
"@search.highlights": {
"firstname": [
"{crmhit}Dani{/crmhit}"
],
"fullname": [
"{crmhit}Dani{/crmhit} Rodrigues"
]
},
"@search.entityname": "contact",
"@search.objectid": "61dada9b-e21d-eb11-a813-000d3ab85224",
"key": "61dada9b-e21d-eb11-a813-000d3ab85224contact2",
"ownerid": "4a390a3c-df19-eb11-a812-000d3ab85fc1",
"owneridname": "MOD Administrator",
"@search.ownerid.logicalname": "systemuser",
"owningbusinessunit": "0158d183-b119-eb11-a812-000d3ab85fc1",
"owningbusinessunitname": "",
"@search.owningbusinessunit.logicalname": "businessunit",
"sharedtoprincipalid": [],
"@search.objecttypecode": 2,
"fullname": "Dani Rodrigues",
"versionnumber": 3552525,
"statecode@stringcollection": [
"Active"
],
"statecode": 0,
"statuscode@stringcollection": [
"Active"
],
"statuscode": 1,
"entityimage_url": null,
"lastsyncdate": "/Date(1604853700592)/",
"createdon": "11/3/2020 3:41 PM",
"modifiedon": "11/3/2020 3:41 PM",
"documentbody": null,
"body": null,
"filebody": null,
"emailaddress1": null,
"address1_city": null,
"telephone1": null,
"address1_telephone1": null,
"parentcustomerid": null,
"parentcustomeridname": null,
"donotbulkpostalmail": false,
"donotbulkemail": false,
"donotpostalmail": false,
"donotfax": false,
"donotemail": false,
"department": null
},
{
"@search.score": 0.6330543,
"@search.highlights": {
"firstname": [
"{crmhit}Danny{/crmhit}"
],
"fullname": [
"{crmhit}Danny{/crmhit} Alves Rodrigues"
]
},
"@search.entityname": "contact",
"@search.objectid": "48ff0ab2-e21d-eb11-a813-000d3ab85224",
"key": "48ff0ab2-e21d-eb11-a813-000d3ab85224contact2",
"ownerid": "4a390a3c-df19-eb11-a812-000d3ab85fc1",
"owneridname": "MOD Administrator",
"@search.ownerid.logicalname": "systemuser",
"owningbusinessunit": "0158d183-b119-eb11-a812-000d3ab85fc1",
"owningbusinessunitname": "",
"@search.owningbusinessunit.logicalname": "businessunit",
"sharedtoprincipalid": [],
"@search.objecttypecode": 2,
"fullname": "Danny Alves Rodrigues",
"versionnumber": 3552575,
"statecode@stringcollection": [
"Active"
],
"statecode": 0,
"statuscode@stringcollection": [
"Active"
],
"statuscode": 1,
"entityimage_url": null,
"lastsyncdate": "/Date(1604853700608)/",
"createdon": "11/3/2020 3:41 PM",
"modifiedon": "11/3/2020 3:41 PM",
"documentbody": null,
"body": null,
"filebody": null,
"emailaddress1": null,
"address1_city": null,
"telephone1": null,
"address1_telephone1": null,
"parentcustomerid": null,
"parentcustomeridname": null,
"donotbulkpostalmail": false,
"donotbulkemail": false,
"donotpostalmail": false,
"donotfax": false,
"donotemail": false,
"department": null
}
],
"facets": {
"@search.entityname": [
{
"Type": "Value",
"Value": "contact",
"Count": 2
}
]
},
"totalrecordcount": 2
}
And here is the view Quick Find Active Contacts:
Suggestion
Suggestion is another new feature of the Relevance Search API. With the HTTP POST endpoint [Organization URI]/api/search/v1.0/suggest, a list of records where their's primary field matches the input term will be returned.
This is different from a regular search request because a suggestion search only searches through an entity record’s primary field, while search requests search through all relevance search-enabled entity fields.
For example, a search engine that implements such a feature can display suggestions when the user has entered at least 3 characters in the search input.
The input parameters are not the same for the "search" endpoint. For example, it's not possible to use the Lucene syntax query. And it makes sense since the search is only made upon records' primary fields. The exhaustive list of input parameters is described in Microsoft's documentation. Probably the most important parameter is the "usefuzzy" parameter that contains a boolean value to use or not fuzzy search.
To compare with a search query, the following request:
POST [Organization URI]/api/search/v1.0/suggest
{
"search": "danni",
"usefuzzy": true
}
returns the following records:
Fullname | text |
---|---|
Danni Rodriguez |
{crmhit}Danni{/crmhit} Rodriguez |
Dani Rodrigues |
{crmhit}Dani{/crmhit} Rodrigues |
Danny Alves Rodrigues |
{crmhit}Danny{/crmhit} Alves Rodrigues |
Complete JSON:
{
"value": [
{
"text": "{crmhit}Danni{/crmhit} Rodriguez",
"document": {
"@search.objectid": "c8527da4-e21d-eb11-a813-000d3ab85224",
"@search.entityname": "contact",
"@search.objecttypecode": 2,
"fullname": "Danni Rodriguez",
"entityimage_url": null,
"emailaddress1": null,
"address1_city": null,
"telephone1": null,
"address1_telephone1": null,
"parentcustomerid": null,
"parentcustomeridname": null,
"donotbulkpostalmail": false,
"donotbulkemail": false,
"donotpostalmail": false,
"donotfax": false,
"donotemail": false,
"department": null,
"territorycode": [
"Default Value"
],
"familystatuscode": [],
"paymenttermscode": []
}
},
{
"text": "{crmhit}Dani{/crmhit} Rodrigues",
"document": {
"@search.objectid": "61dada9b-e21d-eb11-a813-000d3ab85224",
"@search.entityname": "contact",
"@search.objecttypecode": 2,
"fullname": "Dani Rodrigues",
"entityimage_url": null,
"emailaddress1": null,
"address1_city": null,
"telephone1": null,
"address1_telephone1": null,
"parentcustomerid": null,
"parentcustomeridname": null,
"donotbulkpostalmail": false,
"donotbulkemail": false,
"donotpostalmail": false,
"donotfax": false,
"donotemail": false,
"department": null,
"territorycode": [
"Default Value"
],
"familystatuscode": [],
"paymenttermscode": []
}
},
{
"text": "{crmhit}Danny{/crmhit} Alves Rodrigues",
"document": {
"@search.objectid": "48ff0ab2-e21d-eb11-a813-000d3ab85224",
"@search.entityname": "contact",
"@search.objecttypecode": 2,
"fullname": "Danny Alves Rodrigues",
"entityimage_url": null,
"emailaddress1": null,
"address1_city": null,
"telephone1": null,
"address1_telephone1": null,
"parentcustomerid": null,
"parentcustomeridname": null,
"donotbulkpostalmail": false,
"donotbulkemail": false,
"donotpostalmail": false,
"donotfax": false,
"donotemail": false,
"department": null,
"territorycode": [
"Default Value"
],
"familystatuscode": [],
"paymenttermscode": [
"Net 30"
]
}
}
]
}
Note that there is no score in the output. But there is an "orderby" input parameter.
Important point: even if not specified in the documentation, it seems that the fuzzy search uses a distance of 1 and not 2.
Autocomplete
The last of these new features, the autocomplete endpoint provides a single string output based on an entity record's primary field. As for the suggest endpoint, there a query input parameters to customize how the search is to be done, especially the usefuzzy boolean parameter.
Request
POST [Organization URI]/api/search/v1.0/autocomplete
{
“search”: ”dan”
}
Response
{
"value": "{crmhit}daniel{/crmhit}"
}
Conclusion
The new capabilities offered with this Relevance Search API offer a new perspective to querying data in the Common Data Service world. Developers can leverage this API to create a search engine tailored to the need of the users. And thanks to the fuzzy search and other intelligent features, the results are more precise and do required users that corrected their typos or misspellings.
Ressources
- Microsoft's blog post for the Relevance Search API release
- Microsoft's documentation for the Relevance Search API
- Microsoft's blog post for the search experience release
- Microsoft's documentation for the Lucene query syntax
*This post is locked for comments