[react-search-refiners] Migrated to SPFx 1.7.0 + Added LUIS Azure Function (#677)

* * Migrated to SPFx 1.7.0
* Fixed sort feature
* Added a sample TypeScript function to demonstrate NLP processing for the search query
* Miscelleanous improvements

* * Fixed wrong ids and dependencies

* * Updated README
This commit is contained in:
Franck Cornu 2018-11-13 10:08:20 -05:00 committed by Mikael Svenson
parent fbb928831d
commit bee8dc5c5a
160 changed files with 24701 additions and 8898 deletions

View File

@ -1,12 +0,0 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.6.0-plusbeta",
"libraryName": "react-search-refiners",
"libraryId": "890affef-33e0-4d72-bd72-36399e02143b",
"environment": "spo",
"isCreatingSolution": true,
"componentType": "webpart",
"extensionType": "ApplicationCustomizer",
"packageManager": "npm"
}
}

View File

@ -1,16 +1,39 @@
# SharePoint Framework search with search box, refiners and paging sample
## Summary
This sample shows you how to build user friendly SharePoint search experiences using Office UI fabric tiles, custom refiners, paging and suggestions.
This sample shows you how to build user friendly SharePoint search experiences using SPFx in the modern interface. The main features include:
- Fully customizable SharePoint search query like the good old Content Search Web Part.
- Can either use a static query or be connected to a search box component using SPFx dynamic data.
- Live templating system with Handlebar to meet your requirements in terms of UI + builtin list and tiles templates. Can alos use template from an external file.
- Search results includings previews for Office documents and Office 365 videos.
- Customizable refiners supporting multilingual values for taxonomy based filters.
- Sortable results (unique field).
- Results paging.
- SharePoint best bets support.
- Search query enhancement with NLP tools (like Microsoft LUIS).
<p align="center">
<img src="./images/react-search-refiners.gif"/>
</p>
An associated [blog post](http://thecollaborationcorner.com/2017/10/16/build-dynamic-sharepoint-search-experiences-with-refiners-and-paging-with-spfx-office-ui-fabric-and-pnp-js-library/) is available to give you more details about this sample implementation.
This sample includes the following components and service(s):
**Web Part(s)**
Component | Description
----- | -----
Search Box Web Part | Allows users to enter free text/KQL search queries connected to a search results Web Part.
Search Results Web Part | Performs static or dynamic search query with customizable parameters like refiners, sorting and templating. An associated [blog post](http://thecollaborationcorner.com/2017/10/16/build-dynamic-sharepoint-search-experiences-with-refiners-and-paging-with-spfx-office-ui-fabric-and-pnp-js-library/) is available to give you more details about this Web Part implementation.
**Back-end service(s)**
Service | Description
----- | -----
Search Query Enhancer | Sample Azure function to demonstrate the use of Microsoft LUIS and other cognitive services to interpret user intents and enhance the search box query accordingly.
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-1.6.0--plusbeta-blue.svg)
![drop](https://img.shields.io/badge/drop-1.7.0-green.svg)
## Applies to
@ -33,10 +56,11 @@ Version|Date|Comments
1.3 | Apr1, 2018 | Added the result count + entered keywords option
1.4 | May 10, 2018 | <ul><li>Added the query suggestions feature to the search box Web Part</li><li>Added the automatic translation for taxonomy filter values according to the current site locale.</li> <li>Added the option in the search box Web Part to send the query to an other page</ul>
1.5 | Jul 2, 2018 | <ul><li>Added a templating feature for search results with Handlebars inspired by the [react-content-query-webpart](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-content-query-webpart) sample.</li><li>Upgraded to 1.5.1-plusbeta to use the new SPFx dynamic data feature instead of event aggregator for Web Parts communication.</li> <li>Code refactoring and reorganization.</ul>
2.0.0.5 | Sept 18, 2018 | <ul><li>Upgraded to 1.6.0-plusbeta.</li><li>Added dynamic loading of parts needed in edit mode to reduce web part footprint.</li><li>Added configuration to sort.</li><li>Added option to set web part title.</li><li>Added result count tokens.</li><li>Added toggel to load/use handlebars helpers/moment.</li></ul>
2.0.0.5 | Sept 18, 2018 | <ul><li>Upgraded to 1.6.0-plusbeta.</li><li>Added dynamic loading of parts needed in edit mode to reduce web part footprint.</li><li>Added configuration to sort.</li><li>Added option to set web part title.</li><li>Added result count tokens.</li><li>Added toggle to load/use handlebars helpers/moment.</li></ul>
2.1.0.0 | Oct 14, 2018 | <ul><li>Bug fixes ([#641](https://github.com/SharePoint/sp-dev-fx-webparts/issues/641),[#642](https://github.com/SharePoint/sp-dev-fx-webparts/issues/642))</li><li>Added document and Office 365 videos previews for the list template.</li><li>Added SharePoint best bets support.</li></ul>
2.1.1.0 | Oct 30, 2018 | <ul><li>Bug fix for editing custom template.</li><li>Bug fix for dynamic loading of video helper library.</li><li>Added support for Page context query variables.</li><li>Added `getUniqueCount` helper function.</li></ul>
2.1.2.0 | Nov 9, 2018 | <ul><li>Bug fix for IE11.</li><li>Added date query variables.</li><li>Added support for both result id and query template.</li><li>Added `getUniqueCount` helper function.</li></ul>
2.1.2.0 | Nov 9, 2018 | <ul><li>Bug fix for IE11.</li><li>Added date query variables.</li><li>Added support for both result source id and query template.</li><li>Added `getUniqueCount` helper function.</li></ul>
2.2.0.0 | Nov 11, 2018 | <ul><li>Upgraded to SPFx 1.7.0</li><li>Added a TypeScript Azure Function to demonstrate NLP processing on search query</li><li>Removed extension data source. Now we use the default SPFx 'Page Environment' data source.</li></ul>
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
@ -46,13 +70,47 @@ Version|Date|Comments
## Minimal Path to Awesome
- Clone this repository
### SPFx
- Go to the [spfx](./spfx) directory
- In the command line run:
- `npm install`
- `gulp serve`
### Web Part Configuration ###
### Azure Function (Not mandatory)
The following settings are available in the Web Part property pane:
- Go to the [functions](./functions) directory
- Follow the README.md file instructions
- Set the correct service URL in the Search Box Web Part
## Web Parts Configuration
### Search Box Web Part
<p align="center"><img width="300px" src="./images/sb_property_pane.png"/><p>
#### Default Search Query Settings
Setting | Description
-------|----
Use a dynamic data source | You can set a default query text coming from am other data source. This case is particularly useful when you want to put a search box Web Part on the front page redirecting to an other page with the same query. Use the query string parameter 'q' from the builtin 'Page Environment' data source.
#### Search box options
Setting | Description
-------|----
Enable query suggestions | The search box supports query suggestions from SharePoint. Refer to the following [article](https://docs.microsoft.com/en-us/sharepoint/search/manage-query-suggestions) to know how to add query suggestions in your SharePoint tenant (caution: it can take up to 24h for changes to take effect).
Send the query to a new page | Sends the search query text to a new page. On that page, use an other searh box Web Part configured with a dynamic data source as the default query. This Web Part uses the 'q' query string parameter.
#### Search query enhancement
Setting | Description
-------|----
Use Natural Language Processing service | Turn this option 'on' if you want to enhance the query text with NLP services like LUIS. In the _'Service Url'_ field, enter the URL of the Azure Function endpoint. Refer the instructions in the `'/functions/README.md'` file to set up the service. In this sample, only relevant detected keywords are returned as q new query using LUIS. Enabling debug mode will show you relevant information about the entered query.
---
### Search Results Web Part
<table>
<tr>
@ -68,36 +126,28 @@ The following settings are available in the Web Part property pane:
</tr>
<table>
#### Search Query Configuration ####
#### Search Query Configuration
Setting | Description
-------|----
Search query keywords | Here you choose to use a static search query or a query coming from a search box Web Part on a page or the "q" URL query string parameter. The search query is in KQL format so you can use search query variables (See this [post](http://www.techmikael.com/2015/07/sharepoint-rest-do-support-query.html) to know which ones are allowed). You can only plug one source to this Web Part.
Search query keywords | Here you choose to use a static search query or a query coming from a data source. It is recommended to use the associated Web Part coming with this sample. The search query is in KQL format so you can use search query variables (See this [post](http://www.techmikael.com/2015/07/sharepoint-rest-do-support-query.html) to know which ones are allowed). You can only plug one source to this Web Part.
<p align="center"><img src="./images/wp_connection.png"/><p>
<p align="center"><img width="300px" src="./images/wp_connection.png"/><p>
#### Search Settings ####
#### Search Settings
Setting | Description
-------|----
Query template | The search query template in KQL format. You can use search variables here (like Path:{Site}).
Result Source Identifier | The GUID of a SharePoint result source. If you specify a value here, query template and query keywords won't be applied. Otherwise the default SharePoint result source is used.
Enable Query Rules | Enable the query rules if applies
Selected properties | The search managed properties to retrieve. You can use these properties then in the code like this (`item.property_name`).
Refiners | The search managed properties to use as refiners. Make sure these are refinable. With SharePoint Online, you have to reuse the default ones to do so (RefinableStringXX etc.). The order is the same as they will appear in the refnement panel. You can also provide your own custom labels using the following format RefinableString01:"You custom filter label",RefinableString02:"You custom filter label",...
Result Source Identifier | The GUID of a SharePoint result source.
Initial sort order | The initial search results sort order. You can use mutliple properties here.
Sortable fields | The search managed properties to use for sorting. Make sure these are sortable. With SharePoint Online, you have to reuse the default ones to do so (RefinableStringXX etc.). The order is the same as they will appear in the sort panel. You can also provide your own custom labels using the following format RefinableString01:"You custom filter label",RefinableString02:"You custom filter label",... If no sortable fields are provided, the 'Sort' button will not be visible.
Enable Query Rules | Enable the query rules if applies. Turn this options 'on' to display your SharePoint Promoted results (links only).
Selected properties | The search managed properties to retrieve. You can use these properties then in your Handlebar template with the syntax (`item.property_name`).
Refiners | The search managed properties to use as refiners. Make sure these are refinable. With SharePoint Online, you have to reuse the default ones to do so (RefinableStringXX etc.). The order is the same as they will appear in the refnement panel. You can also provide your own custom labels using the following format RefinableString01:"You custom filter label",RefinableString02:"You custom filter label",... This Web Part supports dynamic translation of taxonomy based refiners with few additional configurations (see below).
Number of items to retrieve per page | Quite explicit. The paging behavior is done directly by the search API (See the *SearchDataProvider.ts* file), not by the code on post-render.
#### Styling Options ####
Setting | Description
-------|----
Show blank if no result | Shows nothing if there is no result
Show result count | Shows the result count and entered keywords
Show paging | Indicates whether or not the component should show the paging control at the bottom.
Result Layouts options | Choose the template to use to display search results. Some layouts are defined by default (List oand Tiles) but you can create your own either by clinkg on the **"Custom"** tile, or **"Edit template"** from an existing chosen template. In custom mode, you can set an external template. It has to be in the same SharePoint tenant. Behind the scenes, the Office UI Fabric core CSS components are used in a isolated way.
### Taxonomy values dynamic translation
##### Miscellaneous: Taxonomy values dynamic translation
This Web Part supports the translation for taxonomy based filters according to current site language. To get it work, you must map a new refinable managed property associated with *ows_taxId_<your_column_name>* crawled property.
@ -105,19 +155,52 @@ This Web Part supports the translation for taxonomy based filters according to c
<img src="./images/managed-property.png"/>
</p>
### Query suggestions
#### Styling Options
The search box supports query suggestions from SharePoint. Refer to the following [article](https://docs.microsoft.com/en-us/sharepoint/search/manage-query-suggestions) to know how to add query suggestions in SharePoint (caution: it can take up to 24h for changes to take effect).
Setting | Description
-------|----
Web Part Title | Shows a title for this Web Part. Set blank if you don't want a title.
Show blank if no result | Shows nothing if there is no result
Show result count | Shows the result count and entered keywords
Show paging | Indicates whether or not the component should show the paging control at the bottom.
Result Layouts options | Choose the template to use to display search results. Some layouts are defined by default (List oand Tiles) but you can create your own either by clinkg on the **"Custom"** tile, or **"Edit template"** from an existing chosen template. In custom mode, you can set an external template. It has to be in the same SharePoint tenant. Behind the scenes, the Office UI Fabric core CSS components are used in a isolated way.
Handlebars Helpers | Load [handlebar helpers](https://github.com/helpers/handlebars-helpers) to use in your template. Disable this option will make Web Part loading faster if you don't need them.
### Templates with Handlebars ###
---
#### Templates with Handlebars
This Web Part allows you change customize the way you display your search results. The templating feature comes directly from the original [react-content-query-webpart](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-content-query-webpart) so thanks to @spplante!
<p align="center">
<img src="./images/edit_template.png"/>
<img width="500px" src="./images/edit_template.png"/>
</p>
##### Available tokens
Setting | Description
-------|----
`{{showResultsCount}}` | Boolean flag corresponding to the associated in the property pane.
`{{totalRows}}` | The result count.
`{{maxResultsCount}}` | The number of results configured to retrieve in the web part.
`{{actualResultsCount}}` | The actual number of results retreived.
`{{keywords}}` | The search query.
`{{getSummary HitHighlightedSummary}}` | Format the *HitHighlightedSummary* property with recognized words in bold.
`{{getDate <date_managed_property> "<format>}}"` | Format the date with moment.ts according to the current language.
`{{getPreviewSrc item}}` | Determine the image thumbnail URL if applicable.
`{{getUrl item}}` | Get the item URL. For a document, it means the URL to the Office Online instance or the direct URL (to download it).
`{{getUrlField managed_propertyOWSURLH "URL/Title"}}` | Return the URL or Title part of a URL field managed property.
`{{getCountMessage totalRows <?keywords>}}` | Display a friendly message displaying the result and the entered keywords.
`{{<search_managed_property_name>}}` | Any valid search managed property returned in the results set. These are typically managed properties set in the *"Selected properties"* setting in the property pane. You don't need to prefix them with `item.` if you are in the "each" loop.
`{{webUrl}}` | The current web relative url. Use `{{../webUrl}}` inside a loop.
`{{siteUrl}}` | The current site relative url. Use `{{../siteUrl}}` inside a loop.
`{{getUniqueCount items "property"}}` | Get the unique count of a property over the result set (or another array)
`{{getUniqueCount array}}` | Get the unique count of objects in an array. Example: [1,1,1,2,2,4] would return `3`.
Also the [Handlebars helpers](https://github.com/helpers/handlebars-helpers) (188 helpers) are also available. You can also define your own in the *BaseTemplateService.ts* file. See [helper-moment](https://github.com/helpers/helper-moment) for date samples using moment.
#### Query variables
The following out of the box query variables are supported/tested:
* {searchTerms}
@ -144,11 +227,11 @@ The following custom query variables are supported:
This WP supports SharePoint best bets via SharePoint query rules:
<p align="center">
<img src="./images/query_rules.png"/>
<img width="500px" src="./images/query_rules.png"/>
</p>
<p align="center">
<img src="./images/best_bets.png"/>
<img width="500px" src="./images/best_bets.png"/>
</p>
#### Elements previews
@ -156,7 +239,7 @@ This WP supports SharePoint best bets via SharePoint query rules:
Previews are available, **only for the list view**, for Office documents and Office 365 videos (not Microsoft Stream). The embed URL is directly taken from the `ServerRedirectedEmbedURL` managed property retrieved from the search results.
<p align="center">
<img src="./images/result_preview.png"/>
<img width="500px" src="./images/result_preview.png"/>
</p>
The WebPart must have the following selected properties in the configuration to get the preview feature work (they are set by default):
@ -168,29 +251,6 @@ The WebPart must have the following selected properties in the configuration to
This preview is displayed as an _iframe_ when the user clicks on the corresponding preview image. DOM manipulations occur to add the _iframe_ container dynamically aside with the _<img/>_ container.
#### Available tokens ####
Setting | Description
-------|----
`{{showResultsCount}}` | Boolean flag corresponding to the associated in the property pane.
`{{totalRows}}` | The result count.
`{{maxResultsCount}}` | The number of results configured to retrieve in the web part.
`{{actualResultsCount}}` | The actual number of results retreived.
`{{keywords}}` | The search query.
`{{getSummary HitHighlightedSummary}}` | Format the *HitHighlightedSummary* property with recognized words in bold.
`{{getDate <date_managed_property> "<format>}}"` | Format the date with moment.ts according to the current language.
`{{getPreviewSrc item}}` | Determine the image thumbnail URL if applicable.
`{{getUrl item}}` | Get the item URL. For a document, it means the URL to the Office Online instance or the direct URL (to download it).
`{{getUrlField managed_propertyOWSURLH "URL/Title"}}` | Return the URL or Title part of a URL field managed property.
`{{getCountMessage totalRows <?keywords>}}` | Display a friendly message displaying the result and the entered keywords.
`{{<search_managed_property_name>}}` | Any valid search managed property returned in the results set. These are typically managed properties set in the *"Selected properties"* setting in the property pane. You don't need to prefix them with `item.` if you are in the "each" loop.
`{{webUrl}}` | The current web relative url. Use `{{../webUrl}}` inside a loop.
`{{siteUrl}}` | The current site relative url. Use `{{../siteUrl}}` inside a loop.
`{{getUniqueCount items "property"}}` | Get the unique count of a property over the result set (or another array)
`{{getUniqueCount array}}` | Get the unique count of objects in an array. Example: [1,1,1,2,2,4] would return `3`.
Also the [Handlebars helpers](https://github.com/helpers/handlebars-helpers) (188 helpers) are also available. You can also define your own in the *BaseTemplateService.ts* file. See [helper-moment](https://github.com/helpers/helper-moment) for date samples using moment.
## Features
This Web Part illustrates the following concepts on top of the SharePoint Framework:
@ -203,4 +263,4 @@ This Web Part illustrates the following concepts on top of the SharePoint Framew
- Use the React container component approach inspiring by the [react-todo-basic sample](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-todo-basic).
- Use [on-el-resize](https://www.npmjs.com/package/on-el-resize) by [Andrew Koltyakov](https://github.com/koltyakov) to resize iframes dynamically
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-search-refiners" />
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-search-refiners" />

View File

@ -1,37 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"search-results": {
"components": [
{
"entrypoint": "./lib/webparts/searchResults/SearchResultsWebPart.js",
"manifest": "./src/webparts/searchResults/SearchResultsWebPart.manifest.json"
}
]
},
"search-box": {
"components": [
{
"entrypoint": "./lib/webparts/searchBox/SearchBoxWebPart.js",
"manifest": "./src/webparts/searchBox/SearchBoxWebPart.manifest.json"
}
]
},
"search-query-string": {
"components": [
{
"entrypoint": "./lib/extensions/queryStringDataSource/QueryStringDataSourceApplicationCustomizer.js",
"manifest": "./src/extensions/queryStringDataSource/QueryStringDataSourceApplicationCustomizer.manifest.json"
}
]
}
},
"localizedResources": {
"SearchWebPartStrings": "lib/webparts/searchResults/loc/{locale}.js",
"PropertyControlStrings": "./node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js",
"SearchBoxWebPartStrings": "lib/webparts/searchBox/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js",
"QueryStringDataSourceApplicationCustomizerStrings": "lib/extensions/queryStringDataSource/loc/{locale}.js"
}
}

View File

@ -1,4 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

@ -1,7 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-search-refiners",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -1,26 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "PnP - Search Web Parts",
"id": "890affef-33e0-4d72-bd72-36399e02143b",
"version": "2.1.2.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": false,
"features": [
{
"title": "Application Extension - Deployment of custom action.",
"description": "Deploys a custom action with ClientSideComponentId association",
"id": "b28f9c2b-6e10-4764-8585-5f8e6533001b",
"version": "1.0.0.0",
"assets": {
"elementManifests": [
"elements.xml"
]
}
}
]
},
"paths": {
"zippedPackage": "solution/pnp-react-search-refiners.sppkg"
}
}

View File

@ -1,24 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
},
"serveConfigurations": {
"default": {
"pageUrl": "https://localhost:5432/workbench"
},
"queryStringDataSource": {
"pageUrl": "https://collaborationcorner.sharepoint.com/teams/PnPIntranet/_layouts/15/workbench.aspx",
"customActions": {
"24cae67d-dec7-4eff-bb41-49451d5b5a11": {
"location": "ClientSideExtension.ApplicationCustomizer",
"properties": {}
}
}
}
}
}

View File

@ -1,4 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -0,0 +1,18 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
dist/
# dependencies
node_modules/
# IDEs and editors
.idea/
# build tools
coverage/
test-report.xml
# misc
npm-debug.log
yarn-error.log

View File

@ -0,0 +1,28 @@
{
"bitwise": true,
"eqeqeq": true,
"forin": true,
"noarg": true,
"noempty": true,
"nonbsp": true,
"nonew": true,
"undef": true,
"varstmt": true,
"esversion": 6,
"latedef": true,
"unused": true,
"indent": 2,
"quotmark": "single",
"maxcomplexity": 20,
"maxlen": 140,
"maxerr": 50,
"globals": {},
"strict": true,
"laxbreak": true,
"browser": true,
"module": true,
"node": true,
"trailing": true,
"onevar": true,
"white": true
}

View File

@ -0,0 +1,22 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Local Azure Function",
"type": "node",
"request": "attach",
"port": 5858,
"sourceMaps": true,
"outFiles": [ "${workspaceRoot}/dist/**/*.js" ]
},
{
"type": "node",
"request": "launch",
"name": "Debug Jest all tests",
"program": "${workspaceRoot}/node_modules/jest/bin/jest",
"args": ["--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
}
]
}

View File

@ -0,0 +1,110 @@
# PnP - Search Query Enhancer
## Description
This sample demonstrates the following principles:
- Create an Azure function using TypeScript and Webpack. The original setup was reused from this [article](https://medium.com/burak-tasci/backend-development-on-azure-functions-with-typescript-56113b6be4b9) with only few adjustments.
- Connect Azure Function to an SPFx component
- Use third party back end services like Microsoft LUIS or Text Analysis to interpret a search query and enhance it with NLP services.
***In this sample, the function is secured by a function code. For production use, refer to [this article](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/use-aadhttpclient-enterpriseapi) to protect and use it with Azure AD and SPFx.***
***In real world scenarios, you may want add your own intents and build your enhanced search queries accordingly. Use this sample as a starter.***
## Why LUIS instead of SharePoint search query rules?
- Easy to manage for power users .They don't have to deal with complex SharePoint concepts. With LUIS, they can manage and refine the model more easily in a friendly comprehensive interface.
- Real time monitoring. Power users can review utterances submitted by end users in the LUIS portal and what keywords are entered. They can add new terms as synonyms automatically from the utterances and identify new intentions more precisely.
- Extensible model with custom intents mapped to predefined well know SharePoint search queries.
- Able to plug in the Bing Spell checker automatically to correct mispeleld words and get a clean query
## Set up the solution
- In the [www.luis.ai](www.luis.ai) portal, imports new applications from the JSON files in the [/luis](./luis) folder.
<p align="center"><img width="500px" src="../images/luis_apps.png"/><p>
- In Azure, create keys for the following Microsoft Cognitive Services:
- Language Understanding
- Bing Spell Check v7
- Text Analytics
<p align="center"><img width="500px" src="../images/azure_keys.png"/><p>
- Fill the following values in the `local.settings.json` file according to your environment:
| Setting | Description
| ------- | ---------
LUIS_SubscriptionKey | The key value for LUIS retrieved from the Azure portal
LUIS_AzureRegion | Azure region where you created the LUIS key
Bing_SpellCheckApiKey | The Bing Spell Check API key retrieved from the Azure portal
TextAnalytics_SubscriptionKey | The key value for Text Analytics Service retrieved from the Azure portal
TextAnalytics_AzureRegion | Azure region where you created the Text Analytics key
- Add keys to your LUIS applications
<p align="center"><img width="500px" src="../images/luis_key_manage.png"/><p>
- Train and publish the LUIS applications
- Fill LUIS app ids in the `luismappings.json` according to your environment
- Play with the function!
### Intents
| Intent | Description
| ------ | -----------
| PnP.SearchByKeywords | The default intent for the search query. Used to improve free text searches for SharePoint (90% of users queries in the portal).
| None | Needed to avoid unrelevant query such as noise words, trolling or insulting words
### Entities
| Entity | Type | Description | Recognition method |
| ------ | ---- | ----------- | ------------ |
| [keyPhrase](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/luis-quickstart-intent-and-key-phrase) | Builtin | This prebuilt enity catches important keywords in the phrase. In this case, we treat these values as a "free" keyword which will be matched with all relevant SharePoint search managed properties. | Machine Learning
## How to debug this function locally ? ##
### Prerequisites ###
- In VSCode, open the root folder `./functions`.
- Install all dependencies using `npm i`.
- Install [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-windows?view=azure-cli-latest) on youre machine.
- Install Azure Function Core tools globaly using `npm install -g azure-functions-core-tools@2` (version 2).
- In a Node.js console, build the solution using `npm run build:dev` cmd. For production use, execute `npm run build` (minified version of the JS code).
- In a Node.js console, from the `pnp-query-enhancer/dist` folder, run the following command `func start`.
- In VSCode, launch the *'Debug Local Azure Function'* debug configuration
- Set breakpoints directly in your **'.ts'** files
- Send your requests either using Postman with the localhost address according to your settings (i.e. `http://localhost:7071/api/enhanceQuery`) or directly in the 'Search Box Webpart' via the 'Service URL' parameter.
- Enjoy ;)
### Azure Function Proxy configuration ###
This solution uses an Azure function proxy to get an only single endpoint URL for multiple functions. See the **proxies.json** file to see defined routes.
## How to deploy the solution to Azure ? ##
### Development scenario
We recommend to use Visual Studio Code to work with this solution.
- In VSCode, download the [Azure Function](https://code.visualstudio.com/tutorials/functions-extension/getting-started) extension
- Sign-in to to Azure account into the extension
- In a Node.js console, build the application using the command `npm run build` (minified version)
- Use the **"Deploy to Function App"** feature (in the extension top bar) using the *'dist'* folder. Make sure you've run the `npm run build` cmd before.
- Upload the application settings (`local.settings.json`)
### Production scenario with CI
A `deploy.ps1` script is available to also deploy this function into your Azure environment.
- From you Azure portal, create a new empty function
- Set the `Azure_Function_Name` value in the `local.settings.json` accordingly.
- Login to Azure using `az login` then run `deploy.ps1` script with your parameters.
***In both scenarios, you can test your function using Postman. If you test it using a SPFx component, don't forget to add the SharePoint domain to the CORS settings to allow this origin:***
<p align="center"><img width="500px" src="../images/cors_settings.png"/><p>

View File

@ -0,0 +1,50 @@
[CmdletBinding()]
Param (
[Parameter(Mandatory = $False)]
[switch]$OverwriteSettings,
[Parameter(Mandatory = $False)]
[switch]$Minified
)
$0 = $myInvocation.MyCommand.Definition
$CommandDirectory = [System.IO.Path]::GetDirectoryName($0)
$AppSettingsFilePath = Join-Path -Path $CommandDirectory -ChildPath "src\local.settings.json"
$AppSettings = Get-Content -Path $AppSettingsFilePath -Raw | ConvertFrom-Json
$ErrorActionPreference = 'Continue'
# Execute tests
npm run test:ci 2>&1 | Write-Host
if ($LASTEXITCODE -eq 1) {
throw "Error during tests!"
}
# Build the solution
$ErrorActionPreference = 'Stop'
if ($Minified.IsPresent) {
npm run build
} else {
npm run build:dev
}
# Deploy the functions
Push-Location '.\dist'
# Get the Azure function name according to the settings
$AzureFunctionName = $AppSettings.Values.Azure_Function_Name
Write-Output "Deploy to function $AzureFunctionName..."
if ($OverwriteSettings.IsPresent) {
# https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local#publish
func azure functionapp publish $AzureFunctionName --publish-local-settings --overwrite-settings
} else {
func azure functionapp publish $AzureFunctionName
}
Pop-Location

View File

@ -0,0 +1,8 @@
<testsuites name="jest tests" tests="2" failures="0" time="6.464">
<testsuite name="POST /api/query/enhance" errors="0" failures="0" skipped="0" timestamp="2018-11-11T22:25:37" time="4.132" tests="2">
<testcase classname="POST /api/query/enhance should throw a warning message if the input query is empty" name="POST /api/query/enhance should throw a warning message if the input query is empty" time="0.005">
</testcase>
<testcase classname="POST /api/query/enhance should throw an error if the language is not supported" name="POST /api/query/enhance should throw an error if the language is not supported" time="0.003">
</testcase>
</testsuite>
</testsuites>

View File

@ -0,0 +1,66 @@
{
"luis_schema_version": "2.2.0",
"versionId": "1.0",
"name": "PnP - SharePoint Search Enhancement (en-us)",
"desc": "PnP - Search keywords and intents recognition (English)",
"culture": "en-us",
"intents": [
{
"name": "None"
},
{
"name": "PnP.SearchByKeywords"
}
],
"entities": [],
"composites": [],
"closedLists": [
],
"regex_entities": [
],
"bing_entities": [],
"model_features": [],
"regex_features": [],
"prebuiltEntities": [
{
"name": "keyPhrase",
"roles": []
}
],
"patterns": [
{
"pattern": "i'm looking for {keyPhrase}",
"intent": "PnP.SearchByKeywords"
},
{
"pattern": "give me infos about {keyPhrase}",
"intent": "PnP.SearchByKeywords"
},
{
"pattern": "{keyPhrase}",
"intent": "PnP.SearchByKeywords"
}
],
"utterances": [
{
"text": "sharepoint documents",
"intent": "PnP.SearchByKeywords",
"entities": []
},
{
"text": "architectural schemas",
"intent": "PnP.SearchByKeywords",
"entities": []
},
{
"text": "i'm looking for azure resources",
"intent": "PnP.SearchByKeywords",
"entities": []
},
{
"text": "office 365 governance plan",
"intent": "PnP.SearchByKeywords",
"entities": []
}
]
}

View File

@ -0,0 +1,68 @@
{
"luis_schema_version": "2.2.0",
"versionId": "1.0",
"name": "PnP - Optimiseur de recherche SharePoint (fr-fr)",
"desc": "PnP - Reconnaissance de mots clés de recherche (Français)",
"culture": "fr-fr",
"intents": [
{
"name": "None"
},
{
"name": "PnP.SearchByKeywords"
}
],
"entities": [
],
"composites": [],
"closedLists": [
],
"regex_entities": [
],
"bing_entities": [],
"model_features": [],
"regex_features": [],
"prebuiltEntities": [
{
"name": "keyPhrase",
"roles": []
}
],
"patterns": [
{
"pattern": "je cherche des informations sur {keyPhrase} et {keyPhrase}",
"intent": "PnP.SearchByKeywords"
},
{
"pattern": "{keyPhrase}",
"intent": "PnP.SearchByKeywords"
},
{
"pattern": "documents sur {keyPhrase}",
"intent": "PnP.SearchByKeywords"
}
],
"utterances": [
{
"text": "documents sur sharepoint",
"intent": "PnP.SearchByKeywords",
"entities": []
},
{
"text": "je cherche des ressources sur azure",
"intent": "PnP.SearchByKeywords",
"entities": []
},
{
"text": "plan de gouvernance office 365",
"intent": "PnP.SearchByKeywords",
"entities": []
},
{
"text": "schémas d'architecture",
"intent": "PnP.SearchByKeywords",
"entities": []
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,74 @@
{
"name": "pnp-search-query-enhancement",
"version": "1.0.0",
"description": "Azure Function to enhance SharePoint search queries",
"repository": {
"type": "git",
"url": ""
},
"keywords": [
"api",
"rest",
"azure-functions",
"azure"
],
"author": {
"name": "Franck Cornu",
"email": "franck.cornu@aequos.ca"
},
"license": "MIT",
"scripts": {
"build": "webpack --config ./tools/build/webpack.prod.js",
"build:dev": "webpack --config ./tools/build/webpack.dev.js",
"lint": "tslint -p ./tsconfig.json --force",
"test": "jest --coverage --colors --verbose",
"test:ci": "jest --ci --coverage --colors",
"release": "standard-version"
},
"dependencies": {
"@types/form-data": "^2.2.1",
"@types/lodash": "^4.14.118",
"@types/node-fetch": "^1.6.9",
"@types/sinon": "^4.3.3",
"@types/sprintf-js": "^1.1.1",
"azure-functions-ts-essentials": "1.3.1",
"clean-webpack-plugin": "^0.1.19",
"form-data": "^2.3.3",
"jest-junit": "^4.0.0",
"lodash": "^4.17.11",
"node-fetch": "^2.2.1",
"path": "^0.12.7",
"sinon": "^5.1.1",
"sprintf-js": "^1.1.1"
},
"devDependencies": {
"@types/jest": "22.1.4",
"@types/node": "^9.4.7",
"awesome-typescript-loader": "~3.4.1",
"copy-webpack-plugin": "~4.3.1",
"cp-cli": "^1.1.2",
"jest": "22.1.4",
"standard-version": "~4.3.0",
"ts-jest": "22.0.4",
"tslint": "~5.9.1",
"typescript": "~2.6.2",
"uglifyjs-webpack-plugin": "^1.1.8",
"webpack": "~3.10.0"
},
"jest": {
"transform": {
"^.+\\.(ts|tsx)$": "<rootDir>/node_modules/ts-jest/preprocessor.js"
},
"testMatch": [
"**/*.spec.ts"
],
"moduleFileExtensions": [
"ts",
"js"
],
"testResultsProcessor": "./node_modules/jest-junit",
"cache": false,
"silent": true,
"testURL": "http://localhost/"
}
}

View File

@ -0,0 +1,16 @@
export interface ILuisMappingsDefinition {
apps: Array<{
appId: string;
language: string;
version: string;
}>;
}
export enum LuisEntities {
KeyPhrase = 'builtin.keyPhrase'
}
export enum LuisIntents {
SearchByKeywords = 'PnP.SearchByKeywords',
None = 'None'
}

View File

@ -0,0 +1,14 @@
{
"apps": [
{
"appId": "0c49d128-4705-4a37-9e62-c7f16e5f8c8e",
"language": "fr",
"version": "1.0"
},
{
"appId": "980e7fbb-9438-48c5-ad77-5ea2e1357d4f",
"language": "en",
"version": "1.0"
}
]
}

View File

@ -0,0 +1,209 @@
import { Context, HttpMethod, HttpRequest, HttpResponse, HttpStatusCode } from 'azure-functions-ts-essentials';
import * as fs from 'fs';
import * as path from 'path';
import { LuisHelper } from '../../helpers/LuisHelper';
import { TextAnalyticsHelper } from '../../helpers/TextAnalyticsHelper';
import { ILuisGetIntentResponse } from '../../models/ILuisGetIntentResponse';
import { LuisIntents, ILuisMappingsDefinition } from './config/ILuisMappingsDefinition';
import { INlpResponse } from '../../models/INlpResponse';
/**
* Routes the request to the default controller using the relevant method.
*/
export async function run(context: Context, req: HttpRequest): Promise<HttpResponse> {
let res: HttpResponse;
switch (req.method) {
case HttpMethod.Get:
break;
case HttpMethod.Post:
// The user raw query from the search box
const rawQuery = req.body
? req.body.rawQuery
: undefined;
// The UI language from the front-end component (i.e SPFx)
const uiLanguage = req.body
? req.body.uiLanguage
: undefined;
// Check if we should use the staging model for LUIS
const isStaging = req.body
? req.body.isStaging
: true;
const textAnalyticsSubscriptionKey = process.env['TextAnalytics_SubscriptionKey'];
const textAnalyticsAzureRegion = process.env['TextAnalytics_AzureRegion'];
const luisSubscriptionKey = process.env['LUIS_SubscriptionKey'];
const luisAuthoringKey = process.env['LUIS_AuthoringKey'];
const luisAzureRegion = process.env['LUIS_AzureRegion'];
const bingSpellCheckSubscriptionKey = process.env['Bing_SpellCheckApiKey'];
const luisMappingFile = process.env['LUIS_MappingsFile'];
// Instanciates helpers
const textAnalyticsHelper = new TextAnalyticsHelper(textAnalyticsSubscriptionKey, textAnalyticsAzureRegion);
const luisHelper = new LuisHelper(luisSubscriptionKey, luisAuthoringKey, luisAzureRegion, bingSpellCheckSubscriptionKey);
try {
// Optimize the query
const response = await enhanceQuery(
rawQuery,
uiLanguage,
isStaging,
luisMappingFile,
luisHelper,
textAnalyticsHelper);
res = {
status: HttpStatusCode.OK,
body: response
};
} catch (error) {
res = {
status: HttpStatusCode.InternalServerError,
body: {
error: {
type: 'function_error',
message: error.message
}
}
};
}
break;
case HttpMethod.Patch:
break;
case HttpMethod.Delete:
break;
default:
res = {
status: HttpStatusCode.MethodNotAllowed,
body: {
error: {
type: 'not_supported',
message: `Method ${req.method} not supported.`
}
}
};
}
return res;
}
/**
* Transform the raw query to an optimzed SharePoint search query using intent recognition an entities extraction
* @param rawQuery the user raw query
* @param uiLanguage the UI language from the front-end component
* @param luisMappingFile the LUIS mappings file path for entities
* @param luisHelper the LUIS helper instance
* @param textAnalyticsHelper the text analytics helepr instance
*/
export async function enhanceQuery(
rawQuery: string,
uiLanguage: string,
isStaging: boolean,
luisMappingFile: string,
luisHelper: LuisHelper,
textAnalyticsHelper: TextAnalyticsHelper): Promise<INlpResponse> {
// Function app JSON default response
let response: INlpResponse;
if (rawQuery) {
// Default values
let debugDetectedLanguage = 'Not recognized (use UI language)';
let sharePointsearchQuery = rawQuery;
// Get LUIS mappings from the configuration file
const mappingFile = fs.readFileSync(path.resolve(__dirname, `./${luisMappingFile}`), { encoding: 'utf8' });
const mappingsData = JSON.parse(mappingFile.toString()) as ILuisMappingsDefinition;
const apps = mappingsData.apps;
try {
// Detect the query language
let detectedLanguage = await textAnalyticsHelper.detectLanguage(rawQuery);
// tslint:disable-next-line:curly
if (detectedLanguage) {
debugDetectedLanguage = detectedLanguage;
// tslint:disable-next-line:curly
} else {
detectedLanguage = uiLanguage;
}
// Select the right LUIS model according to the language
const luisModel = apps.filter(app => {
return app.language === detectedLanguage;
})[0];
// If there is a LUIS model for this language
if (luisModel) {
luisHelper.appId = luisModel.appId;
luisHelper.appVersion = luisModel.version;
luisHelper.isStaging = isStaging;
// Get the top user intent using LUIS
const luisResponse: ILuisGetIntentResponse = await luisHelper.getIntentFromQuery(rawQuery);
// Check if the query has been corrected by the Bing spell checker
if (luisResponse.alteredQuery)
sharePointsearchQuery = luisResponse.alteredQuery;
// Get the user intents from LUIS
switch (luisResponse.topScoringIntent.intent) {
case LuisIntents.SearchByKeywords:
// Get only recognized entities values
// Do here whatever you want with your custom entities and intents
// For this sample, only builtin entities are retrieved
sharePointsearchQuery = luisResponse.entities.map((e) => {
return e.entity;
}).join(" ");
break;
case LuisIntents.None:
break;
default:
break;
}
// Build the response
response = {
alteredQuery: luisResponse.alteredQuery,
topScoringIntent: {
detectedIntent: luisResponse.topScoringIntent.intent,
confidence: luisResponse.topScoringIntent.score,
},
detectedLanguage : debugDetectedLanguage,
entities: luisResponse.entities,
enhancedQuery: sharePointsearchQuery
};
// tslint:disable-next-line:curly
} else {
throw new Error(`The language '${detectedLanguage}' is not supported by this method`);
}
} catch (error) {
throw error;
}
// tslint:disable-next-line:curly
} else {
throw new Error("You can't submit an empty query!");
}
return response;
}

View File

@ -0,0 +1,20 @@
{
"disabled": false,
"bindings": [
{
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"post"
]
},
{
"name": "$return",
"type": "http",
"direction": "out"
}
],
"scriptFile": "enhanceQuery.js"
}

View File

@ -0,0 +1,70 @@
import { Context, HttpMethod, HttpRequest, HttpStatusCode } from 'azure-functions-ts-essentials';
import { TextAnalyticsHelper } from '../../../helpers/TextAnalyticsHelper';
import { run } from '../enhanceQuery';
import * as $ from '../../../../tools/build/helpers';
import * as fs from 'fs';
// Build the process.env object according to Azure Function Application Settings
const localSettingsFile = $.root('./src/local.settings.json');
const settings = JSON.parse(fs.readFileSync(localSettingsFile, { encoding: 'utf8' })
.toString()).Values;
Object.keys(settings)
.map(key => {
process.env[key] = settings[key];
});
describe('POST /api/query/enhance', () => {
it('should throw a warning message if the input query is empty', async () => {
const mockContext: Context = {
done: (err, response) => {
expect(err).toBeUndefined();
expect(response.status).toEqual(HttpStatusCode.InternalServerError);
expect(response.body.error.message).toBeDefined();
}
};
const mockRequest: HttpRequest = {
method: HttpMethod.Post,
headers: { 'content-type': 'application/json' },
body: {
rawQuery: ''
}
};
try {
await run(mockContext, mockRequest);
} catch (e) {
fail(e);
}
});
it('should throw an error if the language is not supported', async () => {
TextAnalyticsHelper.prototype.detectLanguage = jest.fn().mockReturnValue('la');
const mockContext: Context = {
done: (err, response) => {
expect(err).toBeUndefined();
expect(response.status).toEqual(HttpStatusCode.InternalServerError);
expect(response.body.error.message).toBeDefined();
}
};
const mockRequest: HttpRequest = {
method: HttpMethod.Post,
headers: { 'content-type': 'application/json' },
body: {
rawQuery: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
}
};
try {
await run(mockContext, mockRequest);
} catch (e) {
fail(e);
}
});
});

View File

@ -0,0 +1,91 @@
import * as _ from 'lodash';
import fetch, { RequestInit } from 'node-fetch';
import { ILuisGetIntentResponse } from '../models/ILuisGetIntentResponse';
import { Utilities } from './Utilities';
import * as format from 'sprintf-js';
export class LuisHelper {
readonly BASE_URL = 'https://%s.api.cognitive.microsoft.com';
private _subscriptionKey: string;
private _authoringKey: string;
private _baseUrl: string;
private _appId: string;
private _appVersion: string;
private _isStaging: boolean;
private _bingSpellCheckerSubscriptionKey: string;
get appId(): string {
return this._appId;
}
set appId(value: string) {
this._appId = value;
}
get appVersion(): string {
return this._appVersion;
}
set appVersion(value: string) {
this._appVersion = value;
}
get isStaging(): boolean {
return this._isStaging;
}
set isStaging(value: boolean) {
this._isStaging = value;
}
constructor(subscriptionKey: string, authoringKey: string, azureRegion: string, bingSpellCheckerSubscriptionKey?: string) {
this._subscriptionKey = subscriptionKey;
this._authoringKey = authoringKey;
this._baseUrl = format.sprintf(this.BASE_URL, azureRegion);
this._bingSpellCheckerSubscriptionKey = bingSpellCheckerSubscriptionKey ? bingSpellCheckerSubscriptionKey : undefined;
}
async getIntentFromQuery(query: string): Promise<ILuisGetIntentResponse> {
const request: RequestInit = {
headers: {
'Ocp-Apim-Subscription-Key': this._subscriptionKey,
'Content-Type': 'application/json; charset=utf-8'
},
method: 'POST',
body: JSON.stringify(`"${query}"`)
};
let url = `${this._baseUrl}/luis/v2.0/apps/${this._appId}`;
if (this._isStaging)
url = Utilities.addOrReplaceQueryStringParam(url, 'staging', 'true');
if (this._bingSpellCheckerSubscriptionKey) {
url = Utilities.addOrReplaceQueryStringParam(url, 'spellCheck', 'true');
url = Utilities.addOrReplaceQueryStringParam(url, 'bing-spell-check-subscription-key', this._bingSpellCheckerSubscriptionKey);
}
try {
const response = await fetch(url, request);
if (!response.ok) {
throw new Error(await response.text());
} else {
const json = await response.json();
if (json.errors)
throw new Error(json.errors);
return json as ILuisGetIntentResponse;
}
} catch (error) {
throw new Error(error);
}
}
}

View File

@ -0,0 +1,62 @@
import fetch, { RequestInit } from 'node-fetch';
import * as format from 'sprintf-js';
export class TextAnalyticsHelper {
readonly BASE_URL = 'https://%s.api.cognitive.microsoft.com';
readonly SCORE_THRESHOLD = 0.70;
private _subscriptionKey: string;
private _baseUrl: string;
constructor(subscriptionKey: string, azureRegion: string) {
this._subscriptionKey = subscriptionKey;
this._baseUrl = format.sprintf(this.BASE_URL, azureRegion);
}
/**
* Detects the input query language using the Microsoft Text Analytics Service
* @param query the query to analyze
*/
async detectLanguage(query: string): Promise<string> {
const request: RequestInit = {
headers: {
'Ocp-Apim-Subscription-Key': this._subscriptionKey,
Accept: 'application/json',
'Content-Type': 'application/json; charset=utf-8'
},
method: 'POST',
body: JSON.stringify(
{
documents: [{
id: 1,
text: query
}]
})
};
const url = `${this._baseUrl}/text/analytics/v2.0/languages`;
try {
const response = await fetch(url, request);
const json = await response.json();
// tslint:disable-next-line:curly
if (json.statusCode) {
throw new Error(json.message);
} else {
// Get only language with confidence geater than 70%
const isoLanguageName = json.documents[0].detectedLanguages.filter(language => {
return language.score > this.SCORE_THRESHOLD;
});
return isoLanguageName.length > 0 ? isoLanguageName[0].iso6391Name : undefined;
}
} catch (error) {
throw new Error(error);
}
}
}

View File

@ -0,0 +1,30 @@
export class Utilities {
/**
* Add or replace a query string parameter
* @param url The current URL
* @param param The query string parameter to add or replace
* @param value The new value
*/
static addOrReplaceQueryStringParam(url: string, param: string, value: string): string {
// tslint:disable-next-line:prefer-template
const re = new RegExp('[\\?&]' + param + '=([^&#]*)');
const match = re.exec(url);
let delimiter;
let newString;
if (match === null) {
// Append new param
const hasQuestionMark = /\?/.test(url);
delimiter = hasQuestionMark ? '&' : '?';
// tslint:disable-next-line:prefer-template
newString = url + delimiter + param + '=' + value;
} else {
delimiter = match[0].charAt(0);
// tslint:disable-next-line:prefer-template
newString = url.replace(re, delimiter + param + '=' + value);
}
return newString;
}
}

View File

@ -0,0 +1,3 @@
{
"version": "2.0"
}

View File

@ -0,0 +1,19 @@
{
"IsEncrypted": false,
"Values": {
"LUIS_SubscriptionKey": "0065cf294a8244409cf40f1400ed74ab",
"LUIS_AzureRegion": "eastus",
"LUIS_AppConfigFolderPath": ".\\config",
"LUIS_MappingsFile": ".\\config\\luismappings.dev.json",
"Bing_SpellCheckApiKey": "c5895173d71242b6b1c20cdb28362549",
"TextAnalytics_SubscriptionKey": "5332d75141cb4b2c90365299e5262318",
"TextAnalytics_AzureRegion": "canadacentral",
"Azure_Function_Name": "pnpsearchfunction",
"FUNCTIONS_WORKER_RUNTIME": "node",
"NODE_OPTIONS": "--inspect=5858"
},
"Host": {
"LocalHttpPort": 7071,
"CORS": "*"
}
}

View File

@ -0,0 +1,28 @@
import { LuisIntents, LuisEntities } from "../functions/enhanceQuery/config/ILuisMappingsDefinition";
export interface ILuisGetIntentResponse {
query: string;
entities: Array<ILuisResponseEntity>;
alteredQuery?: string;
topScoringIntent: ILuisResponseIntent;
/**
* Intents scores. Only visible in verbose mode
*/
intents?: Array<ILuisResponseIntent>;
}
export interface ILuisResponseIntent {
intent: LuisIntents;
score: number;
}
export interface ILuisResponseEntity {
entity: string;
type: LuisEntities;
startIndex: number;
endIndex: number;
resolution?: {
values: Array<string>;
};
role?: string;
}

View File

@ -0,0 +1,34 @@
import { ILuisResponseEntity, ILuisResponseIntent } from './ILuisGetIntentResponse';
export interface INlpResponse {
/**
* The corrected query is applicable.
*/
alteredQuery: string;
/**
* The detected language of the input query.
*/
detectedLanguage: string;
/**
* The recognized intent from the query
*/
topScoringIntent: INlpIntent;
/**
* The list of entities recognized in the query.
*/
entities: Array<ILuisResponseEntity>;
/**
* The final transformed query.
*/
enhancedQuery: string;
}
export interface INlpIntent {
detectedIntent: string;
confidence: number;
}

View File

@ -0,0 +1,14 @@
{
"$schema": "http://json.schemastore.org/proxies",
"proxies": {
"Optimize": {
"matchCondition": {
"route": "/api/query/enhance",
"methods": [
"POST"
]
},
"backendUri": "http://%WEBSITE_HOSTNAME%/api/enhanceQuery"
}
}
}

View File

@ -0,0 +1,8 @@
const path = require('path');
exports.root = function(args) {
const ROOT = path.resolve(__dirname, '../..');
args = Array.prototype.slice.call(arguments, 0);
return path.join.apply(path, [ROOT].concat(args));
};

View File

@ -0,0 +1,71 @@
const $ = require('./helpers');
const copyWebpackPlugin = require('copy-webpack-plugin');
const cleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
module.exports = {
target: 'node',
entry: {
'enhanceQuery': $.root('./src/functions/enhanceQuery/enhanceQuery.ts')
/* 'anotherFunctionEntryPoint': $.root('./src/functions/anotherFunctionEntryPoint/anotherFunctionEntryPoint.ts'),*/
},
output: {
path: $.root('dist'),
filename: '[name]/[name].js',
libraryTarget: 'commonjs2',
devtoolModuleFilenameTemplate: '[absolute-resource-path]'
},
devtool: 'source-map',
module: {
rules: [
{
test: /\.ts$/,
use: 'awesome-typescript-loader?declaration=false',
exclude: [/\.(spec|e2e)\.ts$/]
}
]
},
resolve: {
extensions: ['.ts', '.js', '.json'],
modules: [
'node_modules',
'src'
]
},
plugins: [
new copyWebpackPlugin([
{
from: 'src/host.json',
to: 'host.json'
},
{
from: 'src/proxies.json',
to: 'proxies.json'
},
{
from: 'src/local.settings.json',
to: 'local.settings.json'
},
{
context: 'src/functions',
from: '**/function.json',
to: ''
},
{
context: 'src/functions',
from: '**/config/*.json',
to: ''
}
]),
new cleanWebpackPlugin(['dist/**/*'], {
allowExternal: true,
root: $.root('.'),
verbose: false
}),
new webpack.IgnorePlugin(/^encoding$/, /node-fetch/)
],
node: {
__filename: false,
__dirname: false,
}
};

View File

@ -0,0 +1,75 @@
const $ = require('./helpers');
const uglifyJSPlugin = require('uglifyjs-webpack-plugin');
const copyWebpackPlugin = require('copy-webpack-plugin');
const cleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
module.exports = {
target: 'node',
entry: {
'enhanceQuery': $.root('./src/functions/enhanceQuery/enhanceQuery.ts')
/* 'anotherFunctionEntryPoint': $.root('./src/functions/anotherFunctionEntryPoint/anotherFunctionEntryPoint.ts'),*/
},
output: {
path: $.root('dist'),
filename: '[name]/[name].js',
libraryTarget: 'commonjs2'
},
module: {
rules: [
{
test: /\.ts$/,
use: 'awesome-typescript-loader?declaration=false',
exclude: [/\.(spec|e2e)\.ts$/]
}
]
},
resolve: {
extensions: ['.ts', '.js', '.json'],
modules: [
'node_modules',
'src'
]
},
plugins: [
new uglifyJSPlugin({
uglifyOptions: {
ecma: 6
}
}),
new copyWebpackPlugin([
{
from: 'src/host.json',
to: 'host.json'
},
{
from: 'src/proxies.json',
to: 'proxies.json'
},
{
context: 'src/functions',
from: '**/function.json',
to: ''
},
{
from: 'src/local.settings.json',
to: 'local.settings.json'
},
{
context: 'src/functions',
from: '**/config/*.json',
to: ''
}
]),
new cleanWebpackPlugin(['dist/**/*'], {
allowExternal: true,
root: $.root('.'),
verbose: false
}),
new webpack.IgnorePlugin(/^encoding$/, /node-fetch/)
],
node: {
__filename: false,
__dirname: false,
}
};

View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es2015",
"module": "commonjs",
"sourceMap": true,
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"suppressImplicitAnyIndexErrors": true,
"lib": [ "es2015"],
},
"include": [
"src/**/*.ts"
],
"awesomeTypescriptLoaderOptions": {
"usePrecompiledFiles": true,
"useWebpackText": true
}
}

View File

@ -0,0 +1,264 @@
{
"rules": {
"no-unnecessary-class": [
true,
"allow-constructor-only",
"allow-static-only",
"allow-empty-class"
],
"member-access": [
true,
"no-public"
],
"member-ordering": [
true,
"public-before-private",
"static-before-instance",
"variables-before-functions"
],
"adjacent-overload-signatures": true,
"unified-signatures": true,
"prefer-function-over-method": [
true,
"allow-public",
"allow-protected"
],
"no-invalid-this": [
true,
"check-function-in-method"
],
"no-duplicate-super": true,
"new-parens": true,
"no-misused-new": true,
"no-construct": true,
"no-empty-interface": true,
"prefer-method-signature": true,
"interface-over-type-literal": true,
"no-arg": true,
"only-arrow-functions": [
true,
"allow-declarations",
"allow-named-functions"
],
"arrow-parens": [
true,
"ban-single-arg-parens"
],
"arrow-return-shorthand": true,
"no-return-await": true,
"prefer-const": true,
"no-shadowed-variable": [
true,
{
"temporalDeadZone": false
}
],
"one-variable-per-declaration": [
true,
"ignore-for-loop"
],
"no-duplicate-variable": [
true,
"check-parameters"
],
"no-unnecessary-initializer": true,
"no-implicit-dependencies": true,
"ordered-imports": [
true,
{
"import-sources-order": "any",
"named-imports-order": "case-insensitive",
"grouped-imports": true
}
],
"no-duplicate-imports": true,
"import-blacklist": [
true,
"rxjs"
],
"no-require-imports": true,
"no-default-export": true,
"no-reference": true,
"typedef": [
true,
"call-signature",
"property-declaration"
],
"no-inferrable-types": true,
"no-angle-bracket-type-assertion": true,
"callable-types": true,
"no-null-keyword": true,
"no-non-null-assertion": true,
"array-type": [
true,
"generic"
],
"prefer-object-spread": true,
"object-literal-shorthand": true,
"object-literal-key-quotes": [
true,
"as-needed"
],
"quotemark": [
true,
"single",
"avoid-template",
"avoid-escape"
],
"prefer-template": true,
"no-invalid-template-strings": true,
"triple-equals": [
true,
"allow-null-check"
],
"binary-expression-operand-order": true,
"no-dynamic-delete": true,
"no-bitwise": true,
"use-isnan": true,
"no-conditional-assignment": true,
"prefer-conditional-expression": [
true,
"check-else-if"
],
"prefer-for-of": true,
"forin": true,
"switch-default": true,
"no-switch-case-fall-through": true,
"no-unsafe-finally": true,
"no-duplicate-switch-case": true,
"encoding": true,
"cyclomatic-complexity": [
true,
20
],
"max-file-line-count": [
true,
1000
],
"max-line-length": [
true,
300
],
"indent": [
true,
"spaces",
2
],
"eofline": true,
"curly": [
true,
],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-module",
"check-separator",
"check-rest-spread",
"check-type",
"check-typecast",
"check-type-operator",
"check-preblock"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
},
{
"call-signature": "onespace",
"index-signature": "onespace",
"parameter": "onespace",
"property-declaration": "onespace",
"variable-declaration": "onespace"
}
],
"space-before-function-paren": [
true,
{
"anonymous": "never",
"named": "never",
"asyncArrow": "always",
"method": "never",
"constructor": "never"
}
],
"space-within-parens": 0,
"import-spacing": true,
"no-trailing-whitespace": true,
"newline-before-return": true,
"newline-per-chained-call": true,
"one-line": [
true,
"check-open-brace",
"check-whitespace",
"check-else",
"check-catch",
"check-finally"
],
"no-consecutive-blank-lines": [
true,
1
],
"semicolon": [
true,
"always",
"strict-bound-class-methods"
],
"align": [
true,
"parameters",
"statements"
],
"trailing-comma": [
true,
{
"multiline": "never",
"singleline": "never",
"esSpecCompliant": true
}
],
"class-name": true,
"variable-name": [
true,
"check-format",
"allow-leading-underscore",
"ban-keywords"
],
"comment-format": [
true,
"check-space"
],
"jsdoc-format": [
true,
"check-multiline-start"
],
"no-redundant-jsdoc": true,
"no-console": [
true,
"log",
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-debugger": true,
"no-eval": true,
"no-string-throw": true,
"no-namespace": true,
"no-internal-module": true,
"radix": true,
"no-unused-expression": [
true,
"allow-fast-null-checks"
],
"no-empty": true,
"no-sparse-arrays": true
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 MiB

After

Width:  |  Height:  |  Size: 7.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -1,61 +0,0 @@
{
"name": "react-search-refiners",
"version": "2.1.0",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/decorators": "1.6.0-plusbeta",
"@microsoft/sp-application-base": "1.6.0-plusbeta",
"@microsoft/sp-core-library": "1.6.0-plusbeta",
"@microsoft/sp-dialog": "1.6.0-plusbeta",
"@microsoft/sp-loader": "1.6.0-plusbeta",
"@microsoft/sp-lodash-subset": "1.6.0-plusbeta",
"@microsoft/sp-office-ui-fabric-core": "1.6.0-plusbeta",
"@microsoft/sp-webpart-base": "1.6.0-plusbeta",
"@pnp/common": "^1.2.5",
"@pnp/logging": "^1.2.5",
"@pnp/odata": "^1.2.5",
"@pnp/polyfill-ie11": "^1.0.0",
"@pnp/sp": "^1.2.5",
"@pnp/spfx-controls-react": "^1.10.0",
"@pnp/spfx-property-controls": "1.11.0",
"@types/es6-promise": "0.0.33",
"@types/fabric": "^1.5.45",
"@types/handlebars": "^4.0.39",
"@types/react": "15.6.6",
"@types/react-dom": "15.5.6",
"@types/sharepoint": "2013.1.9",
"@types/webpack-env": "1.13.1",
"common-tags": "^1.8.0",
"downshift": "1.31.14",
"handlebars": "^4.0.12",
"handlebars-helpers": "^0.8.4",
"immutability-helper": "2.4.0",
"office-ui-fabric-react": "^5.131.2",
"on-el-resize": "0.0.4",
"react": "15.6.2",
"react-ace": "6.1.4",
"react-custom-scrollbars": "4.1.2",
"react-dom": "15.6.2",
"react-js-pagination": "3.0.0",
"video.js": "^7.3.0"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.6.0-plusbeta",
"@microsoft/sp-module-interfaces": "1.6.0-plusbeta",
"@microsoft/sp-webpart-workbench": "1.6.0-plusbeta",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2",
"gulp": "^3.9.1",
"unlazy-loader": "0.1.3",
"webpack-bundle-analyzer": "^2.13.1"
}
}

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<CustomAction
Title="QueryStringDataSource"
Location="ClientSideExtension.ApplicationCustomizer"
ClientSideComponentId="24cae67d-dec7-4eff-bb41-49451d5b5a11"
ClientSideComponentProperties="{&quot;testMessage&quot;:&quot;Test message&quot;}">
</CustomAction>
</Elements>

View File

@ -0,0 +1,44 @@
{
/**
* Install Chrome Debugger Extension for Visual Studio Code to debug your components with the
* Chrome browser: https://aka.ms/spfx-debugger-extensions
*/
"version": "0.2.0",
"configurations": [{
"name": "Local workbench",
"type": "chrome",
"request": "launch",
"url": "https://localhost:4321/temp/workbench.html",
"webRoot": "${workspaceRoot}",
"sourceMaps": true,
"sourceMapPathOverrides": {
"webpack:///../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../../../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../../../../../../src/*": "${webRoot}/src/*"
},
"runtimeArgs": [
"--remote-debugging-port=9222"
]
},
{
"name": "Hosted workbench",
"type": "chrome",
"request": "launch",
"url": "https://collaborationcorner.sharepoint.com/teams/SharePointAADTokenProvider/_layouts/workbench.aspx",
"webRoot": "${workspaceRoot}",
"sourceMaps": true,
"sourceMapPathOverrides": {
"webpack:///../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../../src/*": "${webRoot}/src/*"
},
"runtimeArgs": [
"--remote-debugging-port=9222",
"-incognito"
]
}
]
}

View File

@ -0,0 +1,12 @@
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": false,
"environment": "spo",
"version": "1.7.0",
"libraryName": "react-search-refiners",
"libraryId": "ec742d56-6535-4ef1-85e0-27e2c79a9fb5",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,29 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"search-results-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/searchResults/SearchResultsWebPart.js",
"manifest": "./src/webparts/searchResults/SearchResultsWebPart.manifest.json"
}
]
},
"search-box-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/searchBox/SearchBoxWebPart.js",
"manifest": "./src/webparts/searchBox/SearchBoxWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"SearchResultsWebPartStrings": "lib/webparts/searchResults/loc/{locale}.js",
"SearchBoxWebPartStrings": "lib/webparts/searchBox/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js",
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-search-refiners",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,14 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "PnP - Search Web Parts",
"id": "890affef-33e0-4d72-bd72-36399e02143b",
"version": "2.2.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": false,
"isDomainIsolated": false
},
"paths": {
"zippedPackage": "solution/pnp-react-search-refiners.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -45,6 +45,6 @@ build.configureWebpack.mergeConfig({
}
});
build.webpack.buildConfig
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
build.addSuppression(new RegExp("\[sass\]",'g'));
build.initialize(gulp);

View File

@ -0,0 +1,63 @@
{
"name": "pnp-react-search-refiners",
"version": "2.2.0",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"react": "16.3.2",
"react-dom": "16.3.2",
"@types/react": "16.4.2",
"@types/react-dom": "16.0.5",
"@microsoft/sp-core-library": "1.7.0",
"@microsoft/sp-webpart-base": "1.7.0",
"@microsoft/sp-lodash-subset": "1.7.0",
"@microsoft/sp-office-ui-fabric-core": "1.7.0",
"@types/webpack-env": "1.13.1",
"@types/es6-promise": "0.0.33",
"@microsoft/decorators": "1.7.0",
"@microsoft/sp-application-base": "1.7.0",
"@microsoft/sp-dialog": "1.7.0",
"@microsoft/sp-loader": "1.7.0",
"@pnp/common": "1.2.5",
"@pnp/logging": "1.2.5",
"@pnp/odata": "1.2.5",
"@pnp/sp": "1.2.5",
"@pnp/polyfill-ie11": "1.0.0",
"@pnp/spfx-controls-react": "1.10.0",
"@pnp/spfx-property-controls": "1.11.0",
"@types/fabric": "^1.5.43",
"@types/handlebars": "^4.0.39",
"@types/sharepoint": "2013.1.9",
"common-tags": "^1.8.0",
"downshift": "3.1.5",
"handlebars": "^4.0.12",
"lodash": "4.17.11",
"handlebars-helpers": "^0.8.4",
"immutability-helper": "2.4.0",
"office-ui-fabric-react": "5.120.0",
"on-el-resize": "0.0.4",
"react-ace": "6.1.4",
"react-custom-scrollbars": "4.1.2",
"react-js-pagination": "3.0.0",
"video.js": "^7.3.0"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.7.0",
"@microsoft/sp-tslint-rules": "1.7.0",
"@microsoft/sp-module-interfaces": "1.7.0",
"@microsoft/sp-webpart-workbench": "1.7.0",
"gulp": "~3.9.1",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2",
"unlazy-loader": "0.1.3",
"webpack-bundle-analyzer": "^2.13.1"
}
}

View File

@ -0,0 +1,33 @@
import { HttpClient, HttpClientResponse } from "@microsoft/sp-http";
class ServiceHelper {
private _httpClient: HttpClient;
constructor(httpClient: HttpClient) {
this._httpClient = httpClient;
}
/**
* Ensures an URL can be resolved via a GET or POST request (i.e not 404)
* @param url The URL to test
*/
public async ensureUrlResovles(url: string): Promise<void> {
try {
const responseGet: HttpClientResponse = await this._httpClient.get(url, HttpClient.configurations.v1);
const responsePost = await this._httpClient.post(url, HttpClient.configurations.v1, {});
if ((responseGet.status !== 404) || (responsePost.status !== 404)) {
return;
} else {
throw "Not Found (404)";
}
} catch (error) {
throw error;
}
}
}
export default ServiceHelper;

View File

@ -0,0 +1,19 @@
interface INlpRequest {
/**
* The raw query from the user in the search box
*/
rawQuery: string;
/**
* The current UI language. Used to determine the language for optimization
*/
uiLanguage: string;
/**
* Indicates if we should use the LUIS staging model for optimization
*/
isStaging: boolean;
}
export default INlpRequest;

View File

@ -0,0 +1,52 @@
export interface INlpResponse {
/**
* The detected language of the query ('fr', 'en', etc.)
*/
detectedLanguage: string;
/**
* The corrected query if the original query had grammar syntax mistakes
*/
alteredQuery?: string;
/**
* The recognized intent from the query
*/
topScoringIntent: NlpIntent;
/**
* Recognized entities in the query
*/
entities: INlpEntity[];
/**
* The resulting SharePoint search query
*/
enhancedQuery: string;
}
export interface INlpEntity {
/**
* The LUIS entitiy name
*/
entity: string;
/**
* Type of the entity
*/
type: string;
/**
* All resolutions for the entitiy (i.e. recognized occurences)
*/
resolution: {
values: string[];
};
}
export interface NlpIntent {
detectedIntent: string;
confidence: number;
}

View File

@ -0,0 +1,14 @@
interface ISearchQuery {
/**
* The search query as it appears in the search box input
*/
rawInputValue: string;
/**
* The enhanced query retreived from NLP service
*/
enhancedQuery: string;
}
export default ISearchQuery;

View File

@ -0,0 +1,13 @@
import { INlpResponse } from "../../models/INlpResponse";
interface INlpService {
/**
* Interprets the user search query intents and return the relevant keywords
* @param rawQuery the user raw query input
* @param isStaging indicates if we should use the LUIS staging model
*/
enhanceSearchQuery(rawQuery: string, isStaging: boolean): Promise<INlpResponse>;
}
export default INlpService;

View File

@ -0,0 +1,50 @@
import INlpService from "./INlpService";
import { INlpResponse } from "../../models/INlpResponse";
class MockNlpService implements INlpService {
private _enhancedQueryData: INlpResponse;
constructor() {
// Define mock data
this._enhancedQueryData = {
alteredQuery: null,
detectedLanguage: 'fr',
topScoringIntent: {
confidence: 0.8,
detectedIntent: "Test"
},
entities: [
{
entity: "assurance maladie",
resolution: {
values: [
"b261b287-750e-4699-8472-4ab04ab7601e"
]
},
type:"BNCSearch.NormalizedSubject"
},
{
entity: "maladie",
resolution: {
values: [
"d003c2ff-0352-41d8-8890-b7825352fd83"
]
},
type: "BNCSearch.NormalizedSubject"
}
],
enhancedQuery: `((assurance maladie maladie) OR ("b261b287-750e-4699-8472-4ab04ab7601e" OR "d003c2ff-0352-41d8-8890-b7825352fd83") XRANK(cb=500) (owstaxIdBNCSubject:L0|#b261b287-750e-4699-8472-4ab04ab7601e OR owstaxIdBNCSubject:L0|#d003c2ff-0352-41d8-8890-b7825352fd83)) XRANK(cb=400) (owstaxIdBNCSubject:GPP|#b261b287-750e-4699-8472-4ab04ab7601e OR owstaxIdBNCSubject:GPP|#d003c2ff-0352-41d8-8890-b7825352fd83)`
};
}
/**
* Interprets the user search query intents and return the optimized SharePoint query counterpart
* @param rawQuery the user raw query input
*/
public async enhanceSearchQuery(rawQuery: string): Promise<INlpResponse> {
return this._enhancedQueryData;
}
}
export default MockNlpService;

View File

@ -0,0 +1,65 @@
import { HttpClient } from "@microsoft/sp-http";
import { IWebPartContext } from "@microsoft/sp-webpart-base";
import { ConsoleListener, LogLevel, Logger } from '@pnp/logging';
import INlpService from "./INlpService";
import { INlpResponse } from "../../models/INlpResponse";
import INlpRequest from "../../models/INlpRequest";
class NlpService implements INlpService {
private _serviceUrl: string;
private _spfxContext: IWebPartContext;
constructor(context: IWebPartContext, serviceUrl: string) {
this._serviceUrl = serviceUrl;
this._spfxContext = context;
const consoleListener = new ConsoleListener();
Logger.subscribe(consoleListener);
}
/**
* Interprets the user search query intents and return the optimized SharePoint query counterpart
* @param rawQuery the user raw query input
*/
public async enhanceSearchQuery(rawQuery: string, isStaging: boolean): Promise<INlpResponse> {
const postData: string = JSON.stringify({
rawQuery: rawQuery,
uiLanguage: this._spfxContext.pageContext.cultureInfo.currentUICultureName.split("-")[0],
isStaging: isStaging
} as INlpRequest);
// Make the call to the optimizer service
const url = this._serviceUrl;
const requestHeaders = new Headers();
requestHeaders.append('Accept','application/json;');
requestHeaders.append('Content-Type','application/json; charset=utf-8');
requestHeaders.append('Cache-Control','no-cache');
try {
const results = await this._spfxContext.httpClient.post(url, HttpClient.configurations.v1, {
headers: requestHeaders,
body: postData
});
const response: INlpResponse = await results.json();
if (results.status === 200) {
return response;
} else {
const error = JSON.stringify(response);
Logger.write(`[NlpService.enhanceSearchQuery()]: Error: '${error}' for url '${url}'`, LogLevel.Error);
throw new Error(error);
}
} catch (error) {
const errorMessage = error ? error.message : `Failed to fetch URL '${url}'`;
Logger.write(`[NlpService.enhanceSearchQuery()]: Error: '${errorMessage}' for url '${url}'`, LogLevel.Error);
throw new Error(errorMessage);
}
}
}
export default NlpService;

View File

@ -275,7 +275,7 @@ class SearchService implements ISearchService {
const searchSuggestQuery: SearchSuggestQuery = {
preQuery: true,
querytext: query,
querytext: encodeURIComponent(query.replace(/'/g, '\'\'')),
count: 10,
hitHighlighting: true,
prefixMatch: true,

View File

@ -1,7 +1,7 @@
import ITaxonomyService from './ITaxonomyService';
class MockTaxonomyDataProvider implements ITaxonomyService {
class MockTaxonomyService implements ITaxonomyService {
public initialize(): Promise<void> {
const p1 = new Promise<void>((resolve, reject) => {
@ -16,4 +16,4 @@ class MockTaxonomyDataProvider implements ITaxonomyService {
}
}
export default MockTaxonomyDataProvider;
export default MockTaxonomyService;

View File

@ -1,5 +1,5 @@
import { IWebPartContext } from '@microsoft/sp-webpart-base';
import { Logger, LogLevel, ConsoleListener } from '@pnp/logging';
import { Logger, LogLevel } from '@pnp/logging';
import { SPComponentLoader } from '@microsoft/sp-loader';
import ITaxonomyService from './ITaxonomyService';
import { Text } from '@microsoft/sp-core-library';

View File

@ -5,7 +5,7 @@ import * as Handlebars from 'handlebars';
import { ISearchResult } from '../../models/ISearchResult';
import { html } from 'common-tags';
import { isEmpty, uniqBy, uniq } from '@microsoft/sp-lodash-subset';
import * as strings from 'SearchWebPartStrings';
import * as strings from 'SearchResultsWebPartStrings';
import { Text } from '@microsoft/sp-core-library';
import 'video.js/dist/video-js.css';
import { Logger } from '@pnp/logging';

View File

@ -1,5 +1,5 @@
import { PageOpenBehavior } from '../../helpers/UrlHelper';
import IDynamicDataSourceConnection from '../../models/IDynamicDataSourceConnection';
import { DynamicProperty } from '@microsoft/sp-component-base';
interface ISearchBoxWebPartProps {
searchInNewPage: boolean;
@ -7,7 +7,11 @@ interface ISearchBoxWebPartProps {
openBehavior: PageOpenBehavior;
enableQuerySuggestions: boolean;
useDynamicDataSource: boolean;
sourceInstance: IDynamicDataSourceConnection;
NlpServiceUrl: string;
enableNlpService: boolean;
enableDebugMode: boolean;
isStaging: boolean;
defaultQueryKeywords: DynamicProperty<string>;
}
export default ISearchBoxWebPartProps;

View File

@ -1,5 +1,5 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "096b96cc-8a44-41fa-9b4d-c0ab2ab2a779",
"alias": "SearchBoxWebPart",
"componentType": "WebPart",
@ -21,9 +21,10 @@
"title": {
"default": "Search Box" , "fr-fr": "Boîte de recherche"
},
"description": { "default": "Allows users to enter query keywords", "fr-fr": "Permets aux utilisateurs d'entrer des mots clés de recherche." },
"description": { "default": "Allows users to enter query keywords", "fr-fr": "Permets aux utilisateurs d'entrer des mots clés de recherche." },
"officeFabricIconFontName": "Search",
"properties": {
"defaultQueryKeywords": ""
}
}]
}

View File

@ -0,0 +1,355 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version, Environment, Text, EnvironmentType } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField,
IPropertyPaneField,
PropertyPaneCheckbox,
PropertyPaneDropdown,
PropertyPaneToggle,
PropertyPaneLabel,
IWebPartPropertiesMetadata,
PropertyPaneHorizontalRule,
PropertyPaneDynamicFieldSet,
PropertyPaneDynamicField,
DynamicDataSharedDepth
} from '@microsoft/sp-webpart-base';
import * as strings from 'SearchBoxWebPartStrings';
import ISearchBoxWebPartProps from './ISearchBoxWebPartProps';
import { IDynamicDataCallables, IDynamicDataPropertyDefinition, IDynamicDataSource } from '@microsoft/sp-dynamic-data';
import ISearchQuery from '../../models/ISearchQuery';
import { ISearchBoxContainerProps } from './components/ISearchBoxContainerProps';
import ServiceHelper from '../../helpers/ServiceHelper';
import ISearchService from '../../services/SearchService/ISearchService';
import INlpService from '../../services/NlpService/INlpService';
import MockSearchService from '../../services/SearchService/MockSearchService';
import SearchService from '../../services/SearchService/SearchService';
import MockNlpService from '../../services/NlpService/MockNlpService';
import NlpService from '../../services/NlpService/NlpService';
import { PageOpenBehavior } from '../../helpers/UrlHelper';
import SearchBoxContainer from './components/SearchBoxContainer';
export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWebPartProps> implements IDynamicDataCallables {
private _searchQuery: ISearchQuery;
private _searchService: ISearchService;
private _serviceHelper: ServiceHelper;
private _nlpService: INlpService;
constructor() {
super();
// Initialize default values for search query
this._searchQuery = {
rawInputValue: '',
enhancedQuery: ''
};
}
public render(): void {
let inputValue = this.properties.defaultQueryKeywords.tryGetValue();
if (inputValue && typeof(inputValue) === 'string') {
this._searchQuery.rawInputValue = inputValue;
}
const element: React.ReactElement<ISearchBoxContainerProps> = React.createElement(
SearchBoxContainer, {
onSearch: this._onSearch,
searchInNewPage: this.properties.searchInNewPage,
pageUrl: this.properties.pageUrl,
openBehavior: this.properties.openBehavior,
inputValue: this._searchQuery.rawInputValue,
enableQuerySuggestions: this.properties.enableQuerySuggestions,
searchService: this._searchService,
enableDebugMode: this.properties.enableDebugMode,
enableNlpService: this.properties.enableNlpService,
isStaging: this.properties.isStaging,
NlpService: this._nlpService
} as ISearchBoxContainerProps);
ReactDom.render(element, this.domElement);
}
/**
* Return list of dynamic data properties that this dynamic data source
* returns
*/
public getPropertyDefinitions(): ReadonlyArray<IDynamicDataPropertyDefinition> {
return [
{
id: 'searchQuery',
title: strings.DynamicData.SearchQueryPropertyLabel
},
];
}
/**
* Return the current value of the specified dynamic data set
* @param propertyId ID of the dynamic data set to retrieve the value for
*/
public getPropertyValue(propertyId: string): any {
switch (propertyId) {
case 'searchQuery':
let property = {
[strings.DynamicData.RawInputValuePropertyLabel]: this._searchQuery.rawInputValue
};
if (this.properties.enableNlpService && this.properties.NlpServiceUrl) {
property[strings.DynamicData.EnhancedQueryPropertyLabel] = this._searchQuery.enhancedQuery;
}
return property;
default:
throw new Error('Bad property id');
}
}
protected onInit(): Promise<void> {
this._serviceHelper = new ServiceHelper(this.context.httpClient);
this.context.dynamicDataSourceManager.initializeSource(this);
this.initSearchService();
this.initNlpService();
return Promise.resolve();
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
groups: [
{
groupName: strings.SearchBoxQuerySettings,
groupFields: this._getSearchQueryFields()
},
{
groupName: strings.SearchBoxNewPage,
groupFields: this._getSearchBehaviorOptionsFields()
},
{
groupName: strings.SearchBoxQueryNlpSettings,
groupFields: this._getSearchQueryOptimizationFields()
},
],
displayGroupsAsAccordion: true
}
]
};
}
protected onPropertyPaneFieldChanged(propertyPath: string) {
this.initSearchService();
this.initNlpService();
}
/**
* Handler used to notify data source subscribers when the input query is updated
*/
private _onSearch = (searchQuery: ISearchQuery): void => {
this._searchQuery = searchQuery;
this.context.dynamicDataSourceManager.notifyPropertyChanged('searchQuery');
}
/**
* Verifies if the string is a correct URL
* @param value the URL to verify
*/
private _validatePageUrl(value: string) {
if ((!/^(https?):\/\/[^\s/$.?#].[^\s]*/.test(value) || !value) && this.properties.searchInNewPage) {
return strings.SearchBoxUrlErrorMessage;
}
return '';
}
/**
* Ensures the service URL is valid
* @param value the service URL
*/
private async _validateServiceUrl(value: string) {
if ((!/^(https?):\/\/[^\s/$.?#].[^\s]*/.test(value) || !value)) {
return strings.SearchBoxUrlErrorMessage;
} else {
if (Environment.type !== EnvironmentType.Local) {
try {
await this._serviceHelper.ensureUrlResovles(value);
return '';
} catch (errorMessage) {
return Text.format(strings.UrlNotResolvedErrorMessage, value, errorMessage);
}
} else {
return '';
}
}
}
/**
* Initializes the query suggestions data provider instance according to the current environnement
*/
private initSearchService() {
if (this.properties.enableQuerySuggestions) {
if (Environment.type === EnvironmentType.Local ) {
this._searchService = new MockSearchService();
} else {
this._searchService = new SearchService(this.context);
return "";
}
}
}
/**
* Initializes the query optimization data provider instance according to the current environment
*/
private initNlpService() {
if (this.properties.enableNlpService && this.properties.NlpServiceUrl) {
if (Environment.type === EnvironmentType.Local) {
this._nlpService = new MockNlpService();
this.properties.NlpServiceUrl = 'https://localhost:7071/api/example';
} else {
this._nlpService = new NlpService(this.context, this.properties.NlpServiceUrl);
}
}
}
protected get propertiesMetadata(): IWebPartPropertiesMetadata {
return {
'defaultQueryKeywords': {
dynamicPropertyType: 'string'
}
};
}
/**
* Determines the group fields for the search query options inside the property pane
*/
private _getSearchQueryFields(): IPropertyPaneField<any>[] {
// Sets up search query fields
let searchQueryConfigFields: IPropertyPaneField<any>[] = [
PropertyPaneCheckbox('useDynamicDataSource', {
checked: false,
text: strings.DynamicData.UseDynamicDataSourceLabel,
})
];
if (this.properties.useDynamicDataSource) {
searchQueryConfigFields.push(
PropertyPaneDynamicFieldSet({
label: strings.DynamicData.DefaultQueryKeywordsPropertyLabel,
fields: [
PropertyPaneDynamicField('defaultQueryKeywords', {
label: strings.DynamicData.DefaultQueryKeywordsPropertyLabel,
})
],
sharedConfiguration: {
depth: DynamicDataSharedDepth.Source,
}
})
);
}
return searchQueryConfigFields;
}
/**
* Determines the group fields for the search options inside the property pane
*/
private _getSearchBehaviorOptionsFields(): IPropertyPaneField<any>[] {
let searchBehaviorOptionsFields: IPropertyPaneField<any>[] = [
PropertyPaneToggle("enableQuerySuggestions", {
checked: false,
label: strings.SearchBoxEnableQuerySuggestions
}),
PropertyPaneHorizontalRule(),
PropertyPaneCheckbox('searchInNewPage', {
text: strings.SearchBoxSearchInNewPageLabel
})
];
if (this.properties.searchInNewPage) {
searchBehaviorOptionsFields = searchBehaviorOptionsFields.concat([
PropertyPaneTextField('pageUrl', {
disabled: !this.properties.searchInNewPage,
label: strings.SearchBoxPageUrlLabel,
onGetErrorMessage: this._validatePageUrl.bind(this)
}),
PropertyPaneDropdown('openBehavior', {
label: strings.SearchBoxPageOpenBehaviorLabel,
options: [
{ key: PageOpenBehavior.Self, text: strings.SearchBoxSameTabOpenBehavior, index: 0 },
{ key: PageOpenBehavior.NewTab, text: strings.SearchBoxNewTabOpenBehavior, index: 1 }
],
disabled: !this.properties.searchInNewPage,
selectedKey: 0
})
]);
}
return searchBehaviorOptionsFields;
}
/**
* Determines the group fields for the search query optimization inside the property pane
*/
private _getSearchQueryOptimizationFields(): IPropertyPaneField<any>[] {
let searchQueryOptimizationFields: IPropertyPaneField<any>[] = [
PropertyPaneLabel("", {
text: strings.SearchBoxQueryNlpSettingsDescription
}),
PropertyPaneToggle("enableNlpService", {
checked: false,
label: strings.SearchBoxUserQueryNlpLabel,
})
];
if (this.properties.enableNlpService) {
searchQueryOptimizationFields.push(
PropertyPaneTextField("NlpServiceUrl", {
label: strings.SearchBoxServiceUrlLabel,
disabled: !this.properties.enableNlpService,
onGetErrorMessage: this._validateServiceUrl.bind(this),
description: Text.format(strings.SearchBoxServiceUrlDescription, window.location.host)
}),
PropertyPaneToggle("enableDebugMode", {
checked: false,
label: strings.SearchBoxUseDebugModeLabel,
disabled: !this.properties.enableNlpService,
}),
PropertyPaneToggle("isStaging", {
checked: true,
label: strings.SearchBoxUseStagingEndpoint,
disabled: !this.properties.enableNlpService,
}),
);
} else {
this.properties.enableDebugMode = false;
}
return searchQueryOptimizationFields;
}
}

Some files were not shown because too many files have changed in this diff Show More