├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── CustomSkillForDataIngestion ├── .gitignore ├── Common │ ├── Common.csproj │ ├── Constants.cs │ ├── WebAPISkillContract.cs │ └── WebAPISkillHelper.cs ├── QnAIntegrationCustomSkill.sln ├── QnAIntegrationCustomSkill │ ├── .gitignore │ ├── Assets │ │ ├── DataSource.json │ │ ├── Indexer.json │ │ └── SkillSet.json │ ├── GetKnowledgeBase.cs │ ├── InitializeAccelerator.cs │ ├── Lookup.cs │ ├── Models.cs │ ├── QnAIntegrationCustomSkill.cs │ ├── QnAIntegrationCustomSkill.csproj │ ├── Search.cs │ ├── Suggest.cs │ ├── UploadDocument.cs │ └── host.json └── README.md ├── LICENSE.md ├── README.md ├── SampleDocuments ├── AI enrichment concepts.pdf ├── Analyzers for linguistic and text processing - Azure Cognitive Search _ Microsoft Docs.pdf ├── Choose a pricing tier - Azure Cognitive Search _ Microsoft Docs.pdf ├── Create a suggester - Azure Cognitive Search _ Microsoft Docs.pdf ├── Filter on search results - Azure Cognitive Search _ Microsoft Docs.pdf ├── Fuzzy search - Azure Cognitive Search _ Microsoft Docs.pdf ├── How to model complex data types - Azure Cognitive Search _ Microsoft Docs.pdf ├── How to work with search results - Azure Cognitive Search _ Microsoft Docs.pdf ├── Introduction to Azure Cognitive Search - Azure Cognitive Search _ Microsoft Docs.pdf ├── Monitor operations and activity - Azure Cognitive Search _ Microsoft Docs.pdf ├── Security overview - Azure Cognitive Search _ Microsoft Docs.pdf ├── Service limits for tiers and skus - Azure Cognitive Search _ Microsoft Docs.pdf ├── Simple query syntax - Azure Cognitive Search _ Microsoft Docs.pdf └── Skillset concepts and workflow - Azure Cognitive Search _ Microsoft Docs.pdf ├── SearchUI ├── .env ├── .gitignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── DeploymentInfo.md ├── LICENSE.md ├── NOTICE.txt ├── README.md ├── images │ ├── basic-arch.png │ └── web-app.png ├── package-lock.json ├── package.json ├── public │ ├── config.js │ ├── favicon.ico │ ├── iisnode.yml │ ├── images │ │ ├── QnAMaker.svg │ │ ├── cognitive-search.png │ │ ├── microsoft-small.svg │ │ ├── microsoft.ico │ │ ├── microsoft.svg │ │ ├── msft-logo-small.png │ │ └── search-and-qna.png │ ├── index.html │ ├── robots.txt │ ├── routes.json │ └── web.config ├── src │ ├── App │ │ ├── App.css │ │ └── App.js │ ├── axios.js │ ├── components │ │ ├── AppFooter │ │ │ ├── AppFooter.css │ │ │ └── AppFooter.js │ │ ├── AppHeader │ │ │ ├── AppHeader.css │ │ │ └── AppHeader.js │ │ ├── AppHeaderAuth │ │ │ └── AppHeaderAuth.js │ │ ├── DocumentViewer │ │ │ ├── DocumentViewer.css │ │ │ └── DocumentViewer.js │ │ ├── Facets │ │ │ ├── CheckboxFacet │ │ │ │ ├── CheckboxFacet.css │ │ │ │ └── CheckboxFacet.js │ │ │ ├── Facets.css │ │ │ └── Facets.js │ │ ├── Pager │ │ │ ├── Pager.css │ │ │ └── Pager.js │ │ ├── Results │ │ │ ├── Answer │ │ │ │ ├── Answer.css │ │ │ │ └── Answer.js │ │ │ ├── Result │ │ │ │ ├── Result.css │ │ │ │ └── Result.js │ │ │ ├── Results.css │ │ │ └── Results.js │ │ ├── SearchBar │ │ │ ├── SearchBar.css │ │ │ ├── SearchBar.js │ │ │ └── Suggestions │ │ │ │ ├── Suggestions.css │ │ │ │ └── Suggestions.js │ │ └── Transcript │ │ │ ├── Transcript.css │ │ │ └── Transcript.js │ ├── contexts │ │ └── AuthContext.js │ ├── index.js │ ├── pages │ │ ├── Details │ │ │ ├── Details.css │ │ │ └── Details.js │ │ ├── Home │ │ │ ├── Home.css │ │ │ └── Home.js │ │ ├── Search │ │ │ ├── Search.css │ │ │ └── Search.js │ │ └── Upload │ │ │ ├── Upload.css │ │ │ └── Upload.js │ └── setupTests.js └── styles.css ├── azuredeploy.json ├── images ├── CogSearchQnAMakerArchitecture.jpg ├── deployment-original.png ├── deployment.png ├── initialize-accelerator.png ├── qna-copy-url.png ├── search-results.png └── web-app.png └── linkedazuredeploy.json /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | > Please provide us with the following information: 5 | > --------------------------------------------------------------- 6 | 7 | ### This issue is for a: (mark with an `x`) 8 | ``` 9 | - [ ] bug report -> please search issues before submitting 10 | - [ ] feature request 11 | - [ ] documentation issue or request 12 | - [ ] regression (a behavior that used to work and stopped in a new release) 13 | ``` 14 | 15 | ### Minimal steps to reproduce 16 | > 17 | 18 | ### Any log messages given by the failure 19 | > 20 | 21 | ### Expected/desired behavior 22 | > 23 | 24 | ### OS and Version? 25 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) 26 | 27 | ### Versions 28 | > 29 | 30 | ### Mention any other details that might be useful 31 | 32 | > --------------------------------------------------------------- 33 | > Thanks! We'll be in touch soon. 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | * ... 4 | 5 | ## Does this introduce a breaking change? 6 | 7 | ``` 8 | [ ] Yes 9 | [ ] No 10 | ``` 11 | 12 | ## Pull Request Type 13 | What kind of change does this Pull Request introduce? 14 | 15 | 16 | ``` 17 | [ ] Bugfix 18 | [ ] Feature 19 | [ ] Code style update (formatting, local variables) 20 | [ ] Refactoring (no functional changes, no api changes) 21 | [ ] Documentation content changes 22 | [ ] Other... Please describe: 23 | ``` 24 | 25 | ## How to Test 26 | * Get the code 27 | 28 | ``` 29 | git clone [repo-address] 30 | cd [repo-name] 31 | git checkout [branch-name] 32 | npm install 33 | ``` 34 | 35 | * Test the code 36 | 37 | ``` 38 | ``` 39 | 40 | ## What to Check 41 | Verify that the following are valid 42 | * ... 43 | 44 | ## Other Information 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [project-title] Changelog 2 | 3 | 4 | # x.y.z (yyyy-mm-dd) 5 | 6 | *Features* 7 | * ... 8 | 9 | *Bug Fixes* 10 | * ... 11 | 12 | *Breaking Changes* 13 | * ... 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Cognitive Search Question Answering Solution Accelerator 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 6 | 7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | - [Code of Conduct](#coc) 16 | - [Issues and Bugs](#issue) 17 | - [Feature Requests](#feature) 18 | - [Submission Guidelines](#submit) 19 | 20 | ## Code of Conduct 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Found an Issue? 24 | If you find a bug in the source code or a mistake in the documentation, you can help us by 25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 26 | [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Want a Feature? 29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 30 | Repository. If you would like to *implement* a new feature, please submit an issue with 31 | a proposal for your work first, to be sure that we can use it. 32 | 33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 34 | 35 | ## Submission Guidelines 36 | 37 | ### Submitting an Issue 38 | Before you submit an issue, search the archive, maybe your question was already answered. 39 | 40 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 41 | Help us to maximize the effort we can spend fixing issues and adding new 42 | features, by not reporting duplicate issues. Providing the following information will increase the 43 | chances of your issue being dealt with quickly: 44 | 45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 46 | * **Version** - what version is affected (e.g. 0.1.2) 47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 48 | * **Browsers and Operating System** - is this a problem with all browsers? 49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 50 | * **Related Issues** - has a similar issue been reported before? 51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 52 | causing the problem (line of code or commit) 53 | 54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new]. 55 | 56 | ### Submitting a Pull Request (PR) 57 | Before you submit your Pull Request (PR) consider the following guidelines: 58 | 59 | * Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR 60 | that relates to your submission. You don't want to duplicate effort. 61 | 62 | * Make your changes in a new git fork: 63 | 64 | * Commit your changes using a descriptive commit message 65 | * Push your fork to GitHub: 66 | * In GitHub, create a pull request 67 | * If we suggest changes then: 68 | * Make the required updates. 69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 70 | 71 | ```shell 72 | git rebase master -i 73 | git push -f 74 | ``` 75 | 76 | That's it! Thank you for your contribution! 77 | -------------------------------------------------------------------------------- /CustomSkillForDataIngestion/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.vspscc 94 | *.vssscc 95 | .builds 96 | *.pidb 97 | *.svclog 98 | *.scc 99 | 100 | # Chutzpah Test files 101 | _Chutzpah* 102 | 103 | # Visual C++ cache files 104 | ipch/ 105 | *.aps 106 | *.ncb 107 | *.opendb 108 | *.opensdf 109 | *.sdf 110 | *.cachefile 111 | *.VC.db 112 | *.VC.VC.opendb 113 | 114 | # Visual Studio profiler 115 | *.psess 116 | *.vsp 117 | *.vspx 118 | *.sap 119 | 120 | # Visual Studio Trace Files 121 | *.e2e 122 | 123 | # TFS 2012 Local Workspace 124 | $tf/ 125 | 126 | # Guidance Automation Toolkit 127 | *.gpState 128 | 129 | # ReSharper is a .NET coding add-in 130 | _ReSharper*/ 131 | *.[Rr]e[Ss]harper 132 | *.DotSettings.user 133 | 134 | # TeamCity is a build add-in 135 | _TeamCity* 136 | 137 | # DotCover is a Code Coverage Tool 138 | *.dotCover 139 | 140 | # AxoCover is a Code Coverage Tool 141 | .axoCover/* 142 | !.axoCover/settings.json 143 | 144 | # Coverlet is a free, cross platform Code Coverage Tool 145 | coverage*.json 146 | coverage*.xml 147 | coverage*.info 148 | 149 | # Visual Studio code coverage results 150 | *.coverage 151 | *.coveragexml 152 | 153 | # NCrunch 154 | _NCrunch_* 155 | .*crunch*.local.xml 156 | nCrunchTemp_* 157 | 158 | # MightyMoose 159 | *.mm.* 160 | AutoTest.Net/ 161 | 162 | # Web workbench (sass) 163 | .sass-cache/ 164 | 165 | # Installshield output folder 166 | [Ee]xpress/ 167 | 168 | # DocProject is a documentation generator add-in 169 | DocProject/buildhelp/ 170 | DocProject/Help/*.HxT 171 | DocProject/Help/*.HxC 172 | DocProject/Help/*.hhc 173 | DocProject/Help/*.hhk 174 | DocProject/Help/*.hhp 175 | DocProject/Help/Html2 176 | DocProject/Help/html 177 | 178 | # Click-Once directory 179 | publish/ 180 | 181 | # Publish Web Output 182 | *.[Pp]ublish.xml 183 | *.azurePubxml 184 | # Note: Comment the next line if you want to checkin your web deploy settings, 185 | # but database connection strings (with potential passwords) will be unencrypted 186 | *.pubxml 187 | *.publishproj 188 | 189 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 190 | # checkin your Azure Web App publish settings, but sensitive information contained 191 | # in these scripts will be unencrypted 192 | PublishScripts/ 193 | 194 | # NuGet Packages 195 | *.nupkg 196 | # NuGet Symbol Packages 197 | *.snupkg 198 | # The packages folder can be ignored because of Package Restore 199 | **/[Pp]ackages/* 200 | # except build/, which is used as an MSBuild target. 201 | !**/[Pp]ackages/build/ 202 | # Uncomment if necessary however generally it will be regenerated when needed 203 | #!**/[Pp]ackages/repositories.config 204 | # NuGet v3's project.json files produces more ignorable files 205 | *.nuget.props 206 | *.nuget.targets 207 | 208 | # Microsoft Azure Build Output 209 | csx/ 210 | *.build.csdef 211 | 212 | # Microsoft Azure Emulator 213 | ecf/ 214 | rcf/ 215 | 216 | # Windows Store app package directories and files 217 | AppPackages/ 218 | BundleArtifacts/ 219 | Package.StoreAssociation.xml 220 | _pkginfo.txt 221 | *.appx 222 | *.appxbundle 223 | *.appxupload 224 | 225 | # Visual Studio cache files 226 | # files ending in .cache can be ignored 227 | *.[Cc]ache 228 | # but keep track of directories ending in .cache 229 | !?*.[Cc]ache/ 230 | 231 | # Others 232 | ClientBin/ 233 | ~$* 234 | *~ 235 | *.dbmdl 236 | *.dbproj.schemaview 237 | *.jfm 238 | *.pfx 239 | *.publishsettings 240 | orleans.codegen.cs 241 | 242 | # Including strong name files can present a security risk 243 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 244 | #*.snk 245 | 246 | # Since there are multiple workflows, uncomment next line to ignore bower_components 247 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 248 | #bower_components/ 249 | 250 | # RIA/Silverlight projects 251 | Generated_Code/ 252 | 253 | # Backup & report files from converting an old project file 254 | # to a newer Visual Studio version. Backup files are not needed, 255 | # because we have git ;-) 256 | _UpgradeReport_Files/ 257 | Backup*/ 258 | UpgradeLog*.XML 259 | UpgradeLog*.htm 260 | ServiceFabricBackup/ 261 | *.rptproj.bak 262 | 263 | # SQL Server files 264 | *.mdf 265 | *.ldf 266 | *.ndf 267 | 268 | # Business Intelligence projects 269 | *.rdl.data 270 | *.bim.layout 271 | *.bim_*.settings 272 | *.rptproj.rsuser 273 | *- [Bb]ackup.rdl 274 | *- [Bb]ackup ([0-9]).rdl 275 | *- [Bb]ackup ([0-9][0-9]).rdl 276 | 277 | # Microsoft Fakes 278 | FakesAssemblies/ 279 | 280 | # GhostDoc plugin setting file 281 | *.GhostDoc.xml 282 | 283 | # Node.js Tools for Visual Studio 284 | .ntvs_analysis.dat 285 | node_modules/ 286 | 287 | # Visual Studio 6 build log 288 | *.plg 289 | 290 | # Visual Studio 6 workspace options file 291 | *.opt 292 | 293 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 294 | *.vbw 295 | 296 | # Visual Studio LightSwitch build output 297 | **/*.HTMLClient/GeneratedArtifacts 298 | **/*.DesktopClient/GeneratedArtifacts 299 | **/*.DesktopClient/ModelManifest.xml 300 | **/*.Server/GeneratedArtifacts 301 | **/*.Server/ModelManifest.xml 302 | _Pvt_Extensions 303 | 304 | # Paket dependency manager 305 | .paket/paket.exe 306 | paket-files/ 307 | 308 | # FAKE - F# Make 309 | .fake/ 310 | 311 | # CodeRush personal settings 312 | .cr/personal 313 | 314 | # Python Tools for Visual Studio (PTVS) 315 | __pycache__/ 316 | *.pyc 317 | 318 | # Cake - Uncomment if you are using it 319 | # tools/** 320 | # !tools/packages.config 321 | 322 | # Tabs Studio 323 | *.tss 324 | 325 | # Telerik's JustMock configuration file 326 | *.jmconfig 327 | 328 | # BizTalk build output 329 | *.btp.cs 330 | *.btm.cs 331 | *.odx.cs 332 | *.xsd.cs 333 | 334 | # OpenCover UI analysis results 335 | OpenCover/ 336 | 337 | # Azure Stream Analytics local run output 338 | ASALocalRun/ 339 | 340 | # MSBuild Binary and Structured Log 341 | *.binlog 342 | 343 | # NVidia Nsight GPU debugger configuration file 344 | *.nvuser 345 | 346 | # MFractors (Xamarin productivity tool) working folder 347 | .mfractor/ 348 | 349 | # Local History for Visual Studio 350 | .localhistory/ 351 | 352 | # BeatPulse healthcheck temp database 353 | healthchecksdb 354 | 355 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 356 | MigrationBackup/ 357 | 358 | # Ionide (cross platform F# VS Code tools) working folder 359 | .ionide/ 360 | 361 | # Fody - auto-generated XML schema 362 | FodyWeavers.xsd -------------------------------------------------------------------------------- /CustomSkillForDataIngestion/Common/Common.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 8.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /CustomSkillForDataIngestion/Common/Constants.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | namespace Common 5 | { 6 | public class Constants 7 | { 8 | public const string containerName = "qna-container"; 9 | 10 | public const string dataSourceName = "qna-datasource"; 11 | 12 | public const string indexName = "qna-idx"; 13 | 14 | public const string skillSetName = "qna-skillset"; 15 | 16 | public const string indexerName = "qna-indexer"; 17 | 18 | public const string kbContainerName = "qnamakerdata"; 19 | 20 | public const string qnamakerFolderPath = "data\\QnAMaker"; 21 | 22 | public const string kbIdBlobName = "kbid"; 23 | 24 | public const string keyBlobName = "runtimekey"; 25 | 26 | public const string apiVersion = "2020-06-30"; 27 | 28 | public const int MaxTextFileSizeInMb = 10; 29 | 30 | public const int MaxPdfFileSizeInMb = 25; 31 | 32 | public const int MaxExcelFileSizeInMb = 3; 33 | 34 | public const int MaxDocFileSizeInMb = 10; 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /CustomSkillForDataIngestion/Common/WebAPISkillContract.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE file in the project root for full license information. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace AzureCognitiveSearch.PowerSkills.Common 7 | { 8 | public class WebApiSkillRequest 9 | { 10 | public List Values { get; set; } = new List(); 11 | } 12 | 13 | public class WebApiSkillResponse 14 | { 15 | public List Values { get; set; } = new List(); 16 | } 17 | 18 | public class WebApiRequestRecord 19 | { 20 | public string RecordId { get; set; } 21 | public Dictionary Data { get; set; } = new Dictionary(); 22 | } 23 | 24 | public class WebApiResponseRecord 25 | { 26 | public string RecordId { get; set; } 27 | public Dictionary Data { get; set; } = new Dictionary(); 28 | public List Errors { get; set; } = new List(); 29 | public List Warnings { get; set; } = new List(); 30 | } 31 | 32 | public class WebApiErrorWarningContract 33 | { 34 | public string Message { get; set; } 35 | } 36 | 37 | public class FileReference 38 | { 39 | public string data { get; set; } 40 | } 41 | } -------------------------------------------------------------------------------- /CustomSkillForDataIngestion/Common/WebAPISkillHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE file in the project root for full license information. 3 | 4 | using Microsoft.AspNetCore.Http; 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Linq; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Collections.Specialized; 10 | using System.IO; 11 | using System.Linq; 12 | using System.Net.Http; 13 | using System.Net.Http.Headers; 14 | using System.Threading.Tasks; 15 | using System.Web; 16 | 17 | namespace AzureCognitiveSearch.PowerSkills.Common 18 | { 19 | public static class WebApiSkillHelpers 20 | { 21 | public static bool TestMode = false; 22 | public static Func TestWww; 23 | 24 | public static IEnumerable GetRequestRecords(HttpRequest req) 25 | { 26 | string jsonRequest = new StreamReader(req.Body).ReadToEnd(); 27 | if(String.IsNullOrEmpty(jsonRequest)) 28 | { 29 | return null; 30 | } 31 | WebApiSkillRequest docs = JsonConvert.DeserializeObject(jsonRequest); 32 | return docs.Values; 33 | } 34 | 35 | public static WebApiSkillResponse ProcessRequestRecords(string functionName, IEnumerable requestRecords, Func processRecord) 36 | { 37 | WebApiSkillResponse response = new WebApiSkillResponse(); 38 | 39 | foreach (WebApiRequestRecord inRecord in requestRecords) 40 | { 41 | WebApiResponseRecord outRecord = new WebApiResponseRecord() { RecordId = inRecord.RecordId }; 42 | 43 | try 44 | { 45 | outRecord = processRecord(inRecord, outRecord); 46 | } 47 | catch (Exception e) 48 | { 49 | outRecord.Errors.Add(new WebApiErrorWarningContract() { Message = $"{functionName} - Error processing the request record : {e.ToString() }" }); 50 | } 51 | response.Values.Add(outRecord); 52 | } 53 | 54 | return response; 55 | } 56 | 57 | public static async Task ProcessRequestRecordsAsync(string functionName, IEnumerable requestRecords, Func> processRecord) 58 | { 59 | WebApiSkillResponse response = new WebApiSkillResponse(); 60 | 61 | foreach (WebApiRequestRecord inRecord in requestRecords) 62 | { 63 | WebApiResponseRecord outRecord = new WebApiResponseRecord() { RecordId = inRecord.RecordId }; 64 | 65 | try 66 | { 67 | outRecord = await processRecord(inRecord, outRecord); 68 | } 69 | catch (Exception e) 70 | { 71 | outRecord.Errors.Add(new WebApiErrorWarningContract() { Message = $"{functionName} - Error processing the request record : {e.ToString() }" }); 72 | } 73 | response.Values.Add(outRecord); 74 | } 75 | 76 | return response; 77 | } 78 | 79 | public static async Task> FetchAsync(string uri, string collectionPath) 80 | => await FetchAsync(uri, null, null, collectionPath, HttpMethod.Get); 81 | 82 | public static async Task> FetchAsync(string uri, string apiKeyHeader, string apiKey, string collectionPath) 83 | => await FetchAsync(uri, apiKeyHeader, apiKey, collectionPath, HttpMethod.Get); 84 | 85 | public static async Task> FetchAsync( 86 | string uri, 87 | string collectionPath, 88 | HttpMethod method, 89 | byte[] postBody = null, 90 | string contentType = null) 91 | => await FetchAsync(uri, null, null, collectionPath, method, postBody, contentType); 92 | 93 | public static async Task> FetchAsync( 94 | string uri, 95 | string apiKeyHeader, 96 | string apiKey, 97 | string collectionPath, 98 | HttpMethod method, 99 | byte[] postBody = null, 100 | string contentType = null) 101 | { 102 | using (var client = new HttpClient()) 103 | using (var request = new HttpRequestMessage()) 104 | { 105 | request.Method = method; 106 | request.RequestUri = new Uri(uri); 107 | if (postBody != null) 108 | { 109 | request.Content = new ByteArrayContent(postBody); 110 | } 111 | if (contentType != null) 112 | { 113 | request.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType); 114 | } 115 | if (apiKeyHeader != null) 116 | { 117 | request.Headers.Add(apiKeyHeader, apiKey); 118 | } 119 | 120 | using (HttpResponseMessage response = TestMode ? TestWww(request) : await client.SendAsync(request)) 121 | { 122 | string responseBody = await response.Content.ReadAsStringAsync(); 123 | JObject responseObject = JObject.Parse(responseBody); 124 | 125 | if (!response.IsSuccessStatusCode) 126 | { 127 | throw new HttpRequestException($"The remote service {uri} responded with a {response.StatusCode} error code: {responseObject["message"]?.ToObject()}"); 128 | } 129 | 130 | if (responseObject == null || !(responseObject.SelectToken(collectionPath) is JToken resultsToken)) 131 | { 132 | return Array.Empty(); 133 | } 134 | return resultsToken switch 135 | { 136 | JArray array => array.Children().Select(token => token.ToObject()).ToList(), 137 | _ => new T[] { resultsToken.ToObject() } 138 | }; 139 | } 140 | } 141 | } 142 | 143 | public static string CombineSasTokenWithUri(string uri, string sasToken) 144 | { 145 | // if this data is coming from blob indexer's metadata_storage_path and metadata_storage_sas_token 146 | // then we can simply concat them. But lets use uri builder to be safe and support missing characters 147 | 148 | UriBuilder uriBuilder = new UriBuilder(uri); 149 | NameValueCollection sasParameters = HttpUtility.ParseQueryString(sasToken ?? string.Empty); 150 | var query = HttpUtility.ParseQueryString(uriBuilder.Query); 151 | 152 | foreach (var key in sasParameters.AllKeys) 153 | { 154 | // override this url parameter if it already exists 155 | query[key] = sasParameters[key]; 156 | } 157 | 158 | uriBuilder.Query = query.ToString(); 159 | var finalUrl = uriBuilder.ToString(); 160 | 161 | return finalUrl; 162 | } 163 | } 164 | } -------------------------------------------------------------------------------- /CustomSkillForDataIngestion/QnAIntegrationCustomSkill.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30503.244 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QnAIntegrationCustomSkill", "QnAIntegrationCustomSkill\QnAIntegrationCustomSkill.csproj", "{8E081757-8EF1-43F4-AA97-8CCA1C40393E}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "Common\Common.csproj", "{8D811C88-BCBC-4224-8443-84570C93E66E}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {8E081757-8EF1-43F4-AA97-8CCA1C40393E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {8E081757-8EF1-43F4-AA97-8CCA1C40393E}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {8E081757-8EF1-43F4-AA97-8CCA1C40393E}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {8E081757-8EF1-43F4-AA97-8CCA1C40393E}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {8D811C88-BCBC-4224-8443-84570C93E66E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {8D811C88-BCBC-4224-8443-84570C93E66E}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {8D811C88-BCBC-4224-8443-84570C93E66E}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {8D811C88-BCBC-4224-8443-84570C93E66E}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {8D822709-220B-4B84-9C82-5CC5D0F71376} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /CustomSkillForDataIngestion/QnAIntegrationCustomSkill/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /CustomSkillForDataIngestion/QnAIntegrationCustomSkill/Assets/DataSource.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"{{datasourcename}}", 3 | "description": "QnA docs", 4 | "type":"azureblob", 5 | "credentials":{ 6 | "connectionString":"{{connectionString}}" 7 | }, 8 | "container":{ 9 | "name":"{{containerName}}" 10 | } 11 | } -------------------------------------------------------------------------------- /CustomSkillForDataIngestion/QnAIntegrationCustomSkill/Assets/Indexer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{indexer-name}}", 3 | "targetIndexName": "{{index-name}}", 4 | "dataSourceName": "{{datasource-name}}", 5 | "skillsetName": "{{skillset-name}}", 6 | "schedule": { 7 | "interval": "PT5M" 8 | }, 9 | "disabled": false, 10 | "parameters": { 11 | "batchSize": 50, 12 | "maxFailedItems": -1, 13 | "maxFailedItemsPerBatch": -1, 14 | "configuration": { 15 | "indexedFileNameExtensions": ".pdf,.docx,.doc,.xlsx,.xls,.html,.rtf,.txt,.tsv" 16 | } 17 | }, 18 | "fieldMappings" : [ 19 | { 20 | "sourceFieldName": "metadata_storage_path", 21 | "targetFieldName": "metadata_storage_path" 22 | }, 23 | { 24 | "sourceFieldName" : "metadata_storage_path", 25 | "targetFieldName" : "id", 26 | "mappingFunction" : { "name" : "base64Encode" } 27 | }, 28 | { 29 | "sourceFieldName": "metadata_storage_name", 30 | "targetFieldName": "metadata_storage_name" 31 | }, 32 | { 33 | "sourceFieldName": "metadata_storage_file_extension", 34 | "targetFieldName": "fileType" 35 | } 36 | ], 37 | "outputFieldMappings": [ 38 | { 39 | "sourceFieldName": "/document/content", 40 | "targetFieldName": "content" 41 | }, 42 | { 43 | "sourceFieldName": "/document/status", 44 | "targetFieldName": "status" 45 | }, 46 | { 47 | "sourceFieldName": "/document/keyPhrases", 48 | "targetFieldName": "keyPhrases" 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /CustomSkillForDataIngestion/QnAIntegrationCustomSkill/Assets/SkillSet.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{skillset-name}}", 3 | "description": "Skillset", 4 | "skills": [ 5 | { 6 | "@odata.type": "#Microsoft.Skills.Custom.WebApiSkill", 7 | "name": "UploadToQnA", 8 | "description": "Upload the documents to the QnA knowledge base", 9 | "uri": "https://{{function-name}}.azurewebsites.net/api/upload-to-qna?code={{function-code}}", 10 | "httpMethod": "POST", 11 | "context": "/document", 12 | "batchSize": 50, 13 | "timeout": "PT230S", 14 | "degreeOfParallelism": 1, 15 | "inputs": [ 16 | { 17 | "name": "id", 18 | "source": "/document/id" 19 | }, 20 | { 21 | "name": "blobName", 22 | "source": "/document/metadata_storage_name" 23 | }, 24 | { 25 | "name": "blobUrl", 26 | "source": "/document/metadata_storage_path" 27 | }, 28 | { 29 | "name": "sasToken", 30 | "source": "/document/metadata_storage_sas_token" 31 | }, 32 | { 33 | "name": "blobSize", 34 | "source": "/document/metadata_storage_size" 35 | } 36 | ], 37 | "outputs": [ 38 | { 39 | "name": "status" 40 | } 41 | ] 42 | }, 43 | { 44 | "@odata.type": "#Microsoft.Skills.Text.KeyPhraseExtractionSkill", 45 | "name": "KeyPhraseExtractionSkill", 46 | "context": "/document", 47 | "inputs": [ 48 | { 49 | "name": "text", 50 | "source": "/document/content" 51 | } 52 | ], 53 | "outputs": [ 54 | { 55 | "name": "keyPhrases", 56 | "targetName": "keyPhrases" 57 | } 58 | ] 59 | } 60 | ], 61 | "cognitiveServices": { 62 | "@odata.type": "#Microsoft.Azure.Search.CognitiveServicesByKey", 63 | "description": "CognitiveServices AllInOne account", 64 | "key": "{{cog-svc-allinone-key}}" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /CustomSkillForDataIngestion/QnAIntegrationCustomSkill/GetKnowledgeBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | using System; 5 | using System.IO; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.Azure.WebJobs; 9 | using Microsoft.Azure.WebJobs.Extensions.Http; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.Extensions.Logging; 12 | using Newtonsoft.Json; 13 | using Azure.Storage.Blobs; 14 | using Common; 15 | using Azure.Storage.Blobs.Models; 16 | 17 | namespace QnAIntegrationCustomSkill 18 | { 19 | 20 | 21 | public static class GetKnowledgeBase 22 | { 23 | 24 | private static BlobServiceClient blobServiceClient = new BlobServiceClient(Environment.GetEnvironmentVariable("AzureWebJobsStorage", EnvironmentVariableTarget.Process)); 25 | 26 | [FunctionName("GetKb")] 27 | public static async Task Run( 28 | [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, 29 | ILogger log) 30 | { 31 | log.LogInformation("C# HTTP trigger function processed a request."); 32 | 33 | string kbId = await GetKbId(log); 34 | 35 | GetKbOutput output = new GetKbOutput() 36 | { 37 | QnAMakerKnowledgeBaseID = kbId 38 | }; 39 | 40 | return new OkObjectResult(output); 41 | } 42 | 43 | private static async Task GetKbId(ILogger log) 44 | { 45 | string kbId = string.Empty; 46 | var path = Path.Join(Environment.GetEnvironmentVariable("HOME", EnvironmentVariableTarget.Process), Constants.qnamakerFolderPath); 47 | var filePath = Path.Join(path, Constants.kbIdBlobName + ".txt"); 48 | // Check for kbid in local file system 49 | if (File.Exists(filePath)) 50 | { 51 | kbId = File.ReadAllText(filePath); 52 | } 53 | else 54 | { 55 | BlobContainerClient containerClient = blobServiceClient.GetBlobContainerClient(Constants.kbContainerName); 56 | BlobClient kbidBlobClient = containerClient.GetBlobClient(Constants.kbIdBlobName); 57 | // Check blob for kbid 58 | if (await kbidBlobClient.ExistsAsync()) 59 | { 60 | BlobDownloadInfo download = await kbidBlobClient.DownloadAsync(); 61 | using (var streamReader = new StreamReader(download.Content)) 62 | { 63 | while (!streamReader.EndOfStream) 64 | { 65 | kbId = await streamReader.ReadLineAsync(); 66 | } 67 | } 68 | } 69 | 70 | } 71 | return kbId; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /CustomSkillForDataIngestion/QnAIntegrationCustomSkill/InitializeAccelerator.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | using Azure; 5 | using Azure.Search.Documents.Indexes; 6 | using Azure.Search.Documents.Indexes.Models; 7 | using Azure.Storage.Blobs; 8 | using Common; 9 | using Microsoft.AspNetCore.Http; 10 | using Microsoft.AspNetCore.Mvc; 11 | using Microsoft.Extensions.Logging; 12 | using Microsoft.Azure.CognitiveServices.Knowledge.QnAMaker; 13 | using Microsoft.Azure.CognitiveServices.Knowledge.QnAMaker.Models; 14 | using Microsoft.Azure.WebJobs; 15 | using Microsoft.Azure.WebJobs.Extensions.Http; 16 | using System; 17 | using System.IO; 18 | using System.Net; 19 | using System.Net.Http; 20 | using System.Text; 21 | using System.Threading.Tasks; 22 | 23 | 24 | namespace AzureCognitiveSearch.QnAIntegrationCustomSkill 25 | { 26 | public static class InitializeAccelerator 27 | { 28 | private static HttpClient httpClient = new HttpClient(); 29 | // initializes the accelerator solution. 30 | [FunctionName("init-accelerator")] 31 | public static async Task Run( 32 | [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req, 33 | ILogger log, ExecutionContext executionContext) 34 | { 35 | var storageConnectionString = GetAppSetting("AzureWebJobsStorage"); 36 | string functionCode; 37 | var queryStrings = req.GetQueryParameterDictionary(); 38 | queryStrings.TryGetValue("code", out functionCode); 39 | var searchServiceEndpoint = $"https://{GetAppSetting("SearchServiceName")}.search.windows.net/"; 40 | var basePath = Path.Join(executionContext.FunctionAppDirectory, "Assets"); 41 | string responseMessage; 42 | 43 | try 44 | { 45 | await CreateContainer(storageConnectionString, log); 46 | await CreateDataSource(storageConnectionString, searchServiceEndpoint, basePath, log); 47 | await CreateIndex(searchServiceEndpoint, log); 48 | await CreateSkillSet(searchServiceEndpoint, basePath, functionCode, log); 49 | await CreateIndexer(searchServiceEndpoint, basePath, log); 50 | 51 | responseMessage = "Initialized accelerator successfully."; 52 | } 53 | catch (Exception e) 54 | { 55 | responseMessage = "Failed to initialize accelerator " + e.Message; 56 | } 57 | 58 | return new OkObjectResult(responseMessage); 59 | } 60 | 61 | 62 | private static async Task CreateContainer(string connectionString, ILogger log) 63 | { 64 | try 65 | { 66 | log.LogInformation("init-accelerator: Creating container " + Constants.containerName); 67 | 68 | BlobServiceClient blobServiceClient = new BlobServiceClient(connectionString); 69 | await blobServiceClient.CreateBlobContainerAsync(Constants.containerName); 70 | await blobServiceClient.CreateBlobContainerAsync(Constants.kbContainerName); 71 | 72 | } 73 | catch (Exception e) 74 | { 75 | log.LogError("init-accelerator: container creation failed " + e.Message); 76 | throw new Exception(e.Message); 77 | } 78 | } 79 | 80 | private static async Task CreateDataSource(string storageConnection, string searchServiceEndpoint, string basePath, ILogger log) 81 | { 82 | log.LogInformation("init-accelerator: Creating data source " + Constants.dataSourceName); 83 | 84 | try 85 | { 86 | string uri = string.Format("{0}/datasources/{1}?api-version={2}", searchServiceEndpoint, Constants.dataSourceName, Constants.apiVersion); 87 | var path = Path.Combine(basePath, "DataSource.json"); 88 | using (StreamReader r = new StreamReader(path)) 89 | { 90 | var body = r.ReadToEnd(); 91 | body = body.Replace("{{datasourcename}}", Constants.dataSourceName); 92 | body = body.Replace("{{connectionString}}", storageConnection); 93 | body = body.Replace("{{containerName}}", Constants.containerName); 94 | 95 | var response = await Put(uri, body); 96 | if (response.StatusCode != HttpStatusCode.Created) 97 | { 98 | var responseBody = await response.Content.ReadAsStringAsync(); 99 | log.LogError("init-accelerator: error while creating data source " + responseBody); 100 | throw new Exception(responseBody); 101 | } 102 | 103 | } 104 | } 105 | catch (Exception e) 106 | { 107 | log.LogError("init-accelerator: error while creating data source " + e.Message); 108 | throw new Exception(e.Message); 109 | } 110 | } 111 | 112 | private static async Task CreateIndex(string searchServiceEndpoint, ILogger log) 113 | { 114 | log.LogInformation("init-accelerator: Creating index " + Constants.indexName); 115 | try 116 | { 117 | var idxclient = new SearchIndexClient(new Uri(searchServiceEndpoint), new AzureKeyCredential(GetAppSetting("SearchServiceApiKey"))); 118 | SearchIndex index = new SearchIndex(Constants.indexName) 119 | { 120 | Fields = 121 | { 122 | new SearchField("content", SearchFieldDataType.String) { IsSearchable = true, IsSortable = false, IsFilterable = false, IsFacetable = false}, 123 | new SearchField("metadata_storage_path", SearchFieldDataType.String) { IsSearchable = true, IsSortable = false, IsFilterable = false, IsFacetable = false }, 124 | new SearchField("id", SearchFieldDataType.String) { IsKey = true, IsSearchable = true, IsSortable = false, IsFilterable = false, IsFacetable = false }, 125 | new SearchField("metadata_storage_name", SearchFieldDataType.String) { IsSearchable = true, IsSortable = false, IsFilterable = false, IsFacetable = false }, 126 | new SearchField("status", SearchFieldDataType.String) { IsSearchable = false, IsSortable = false, IsFilterable = false, IsFacetable = false }, 127 | new SearchField("fileType", SearchFieldDataType.String) { IsSearchable = true, IsSortable = false, IsFilterable = true, IsFacetable = true }, 128 | new SearchField("keyPhrases", SearchFieldDataType.Collection(SearchFieldDataType.String)) { IsSearchable = true, IsSortable = false, IsFilterable = true, IsFacetable = true } 129 | } 130 | }; 131 | 132 | var suggester = new SearchSuggester("sg", new[] { "keyPhrases" }); 133 | index.Suggesters.Add(suggester); 134 | 135 | 136 | await idxclient.CreateIndexAsync(index); 137 | } 138 | catch (Exception e) 139 | { 140 | log.LogError("init-accelerator: Error while creating index " + e.Message); 141 | throw new Exception(e.Message); 142 | } 143 | } 144 | 145 | private static async Task CreateSkillSet(string searchServiceEndpoint, string basePath, string functionCode, ILogger log) 146 | { 147 | log.LogInformation("init-accelerator: Creating Skill Set " + Constants.skillSetName); 148 | try 149 | { 150 | string uri = string.Format("{0}/skillsets/{1}?api-version={2}", searchServiceEndpoint, Constants.skillSetName, Constants.apiVersion); 151 | var path = Path.Combine(basePath, "SkillSet.json"); 152 | using (StreamReader r = new StreamReader(path)) 153 | { 154 | var body = r.ReadToEnd(); 155 | body = body.Replace("{{skillset-name}}", Constants.skillSetName); 156 | body = body.Replace("{{function-name}}", GetAppSetting("WEBSITE_SITE_NAME")); 157 | body = body.Replace("{{function-code}}", functionCode); 158 | body = body.Replace("{{cog-svc-allinone-key}}", GetAppSetting("CogServicesKey")); 159 | 160 | var response = await Put(uri, body); 161 | if (response.StatusCode != HttpStatusCode.Created) 162 | { 163 | var responseBody = await response.Content.ReadAsStringAsync(); 164 | log.LogError("init-accelerator: Error while creating skill set " + responseBody); 165 | throw new Exception(responseBody); 166 | } 167 | 168 | } 169 | } 170 | catch (Exception e) 171 | { 172 | log.LogError("init-accelerator: Error while creating skill set " + e.Message); 173 | throw new Exception(e.Message); 174 | } 175 | 176 | } 177 | 178 | private static async Task CreateIndexer(string searchServiceEndpoint, string basePath, ILogger log) 179 | { 180 | log.LogInformation("init-accelerator: Creating indexer " + Constants.indexerName); 181 | try 182 | { 183 | string uri = string.Format("{0}/indexers/{1}?api-version={2}", searchServiceEndpoint, Constants.indexerName, Constants.apiVersion); 184 | var path = Path.Combine(basePath, "Indexer.json"); 185 | using (StreamReader r = new StreamReader(path)) 186 | { 187 | var body = r.ReadToEnd(); 188 | body = body.Replace("{{indexer-name}}", Constants.indexerName); 189 | body = body.Replace("{{index-name}}", Constants.indexName); 190 | body = body.Replace("{{datasource-name}}", Constants.dataSourceName); 191 | body = body.Replace("{{skillset-name}}", Constants.skillSetName); 192 | 193 | var response = await Put(uri, body); 194 | if (response.StatusCode != HttpStatusCode.Created) 195 | { 196 | var responseBody = await response.Content.ReadAsStringAsync(); 197 | log.LogError("init-accelerator: Error while creating indexer " + responseBody); 198 | throw new Exception(responseBody); 199 | } 200 | 201 | } 202 | } 203 | catch (Exception e) 204 | { 205 | log.LogError("init-accelerator: Error while creating indexer " + e.Message); 206 | throw new Exception(e.Message); 207 | } 208 | 209 | } 210 | 211 | 212 | private static async Task Put(string uri, string body) 213 | { 214 | var key = GetAppSetting("SearchServiceApiKey"); 215 | using (var request = new HttpRequestMessage()) 216 | { 217 | request.Method = HttpMethod.Put; 218 | request.RequestUri = new Uri(uri); 219 | 220 | if (!string.IsNullOrEmpty(body)) 221 | { 222 | request.Content = new StringContent(body, Encoding.UTF8, "application/json"); 223 | } 224 | 225 | request.Headers.Add("api-key", $"{key}"); 226 | request.Headers.Add("content", "application/json"); 227 | 228 | var response = await httpClient.SendAsync(request); 229 | return response; 230 | } 231 | } 232 | 233 | private static string GetAppSetting(string key) 234 | { 235 | return Environment.GetEnvironmentVariable(key, EnvironmentVariableTarget.Process); 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /CustomSkillForDataIngestion/QnAIntegrationCustomSkill/Lookup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | using System; 5 | using System.IO; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.Azure.WebJobs; 9 | using Microsoft.Azure.WebJobs.Extensions.Http; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.Extensions.Logging; 12 | using Newtonsoft.Json; 13 | using Common; 14 | using Azure; 15 | using Azure.Search.Documents; 16 | using Azure.Search.Documents.Models; 17 | using Azure.Storage; 18 | using Azure.Storage.Sas; 19 | using System.Net; 20 | 21 | namespace QnAIntegrationCustomSkill 22 | { 23 | public static class Lookup 24 | { 25 | private static string searchApiKey = Environment.GetEnvironmentVariable("SearchServiceApiKey", EnvironmentVariableTarget.Process); 26 | private static string searchServiceName = Environment.GetEnvironmentVariable("SearchServiceName", EnvironmentVariableTarget.Process); 27 | private static string searchIndexName = Constants.indexName; 28 | 29 | private static string storageAccountName = Environment.GetEnvironmentVariable("StorageAccountName", EnvironmentVariableTarget.Process); 30 | private static string storageAccountKey = Environment.GetEnvironmentVariable("StorageAccountKey", EnvironmentVariableTarget.Process); 31 | private static string storageContainerName = Constants.containerName; 32 | 33 | private static StorageSharedKeyCredential sharedStorageCredentials = new StorageSharedKeyCredential(storageAccountName, storageAccountKey); 34 | 35 | // Create a SearchIndexClient to send create/delete index commands 36 | private static Uri serviceEndpoint = new Uri($"https://{searchServiceName}.search.windows.net/"); 37 | private static AzureKeyCredential credential = new AzureKeyCredential(searchApiKey); 38 | 39 | // Create a SearchClient to load and query documents 40 | private static SearchClient searchClient = new SearchClient(serviceEndpoint, searchIndexName, credential); 41 | 42 | [FunctionName("Lookup")] 43 | public static async Task Run( 44 | [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, 45 | ILogger log) 46 | { 47 | log.LogInformation("C# HTTP trigger function processed a request."); 48 | string id = req.Query["id"]; 49 | string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); 50 | dynamic data = JsonConvert.DeserializeObject(requestBody); 51 | id = id ?? data?.id; 52 | 53 | var response = await searchClient.GetDocumentAsync(id); 54 | 55 | 56 | object metadata_storage_path; 57 | response.Value.TryGetValue("metadata_storage_path", out metadata_storage_path); 58 | var storagePath = metadata_storage_path.ToString(); 59 | var startIndex = storagePath.IndexOf(Constants.containerName) + Constants.containerName.Length + 1; 60 | var blobName = storagePath.Substring(startIndex); 61 | blobName = Uri.UnescapeDataString(blobName); 62 | log.LogInformation(blobName); 63 | 64 | var policy = new BlobSasBuilder 65 | { 66 | Protocol = SasProtocol.HttpsAndHttp, 67 | BlobContainerName = storageContainerName, 68 | BlobName = blobName, 69 | Resource = "b", 70 | StartsOn = DateTimeOffset.UtcNow, 71 | ExpiresOn = DateTimeOffset.UtcNow.AddHours(1), 72 | IPRange = new SasIPRange(IPAddress.None, IPAddress.None) 73 | }; 74 | policy.SetPermissions(BlobSasPermissions.Read); 75 | 76 | var sas = policy.ToSasQueryParameters(sharedStorageCredentials); 77 | 78 | LookupOutput output = new LookupOutput(); 79 | output.document = response.Value; 80 | output.sasToken = sas.ToString(); 81 | 82 | return new OkObjectResult(output); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /CustomSkillForDataIngestion/QnAIntegrationCustomSkill/Models.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | using Azure.Search.Documents.Models; 5 | using Microsoft.Azure.CognitiveServices.Knowledge.QnAMaker.Models; 6 | using Microsoft.Azure.Documents; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Text; 10 | 11 | namespace QnAIntegrationCustomSkill 12 | { 13 | class SearchOutput 14 | { 15 | public long? count { get; set; } 16 | public List> results { get; set; } 17 | public Dictionary> facets { get; set; } 18 | public QnAResult answers { get; set; } 19 | } 20 | 21 | public class QnAResult 22 | { 23 | public QnASearchResult answer { get; set; } 24 | public SearchResult document { get; set; } 25 | } 26 | 27 | public class SearchRequest 28 | { 29 | public string q { get; set; } 30 | public int top { get; set; } 31 | public int skip { get; set; } 32 | public bool getAnswer { get; set; } 33 | public List filters { get; set; } 34 | } 35 | 36 | public class SearchFilter 37 | { 38 | public string field { get; set; } 39 | public string value { get; set; } 40 | } 41 | 42 | public class LookupOutput 43 | { 44 | public string sasToken { get; set; } 45 | public SearchDocument document { get; set; } 46 | 47 | } 48 | 49 | public class GetKbOutput 50 | { 51 | public string QnAMakerKnowledgeBaseID { get; set; } 52 | } 53 | 54 | public class Facet 55 | { 56 | public string key { get; set; } 57 | public List value { get; set; } 58 | } 59 | 60 | public class FacetValue 61 | { 62 | public string value { get; set; } 63 | public long? count { get; set; } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /CustomSkillForDataIngestion/QnAIntegrationCustomSkill/QnAIntegrationCustomSkill.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp3.1 4 | v3 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | PreserveNewest 20 | 21 | 22 | PreserveNewest 23 | 24 | 25 | PreserveNewest 26 | 27 | 28 | PreserveNewest 29 | 30 | 31 | PreserveNewest 32 | Never 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /CustomSkillForDataIngestion/QnAIntegrationCustomSkill/Search.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | using System; 5 | using System.IO; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.Azure.WebJobs; 9 | using Microsoft.Azure.WebJobs.Extensions.Http; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.Extensions.Logging; 12 | using Newtonsoft.Json; 13 | using Common; 14 | using Azure; 15 | using Azure.Search.Documents; 16 | using System.Collections.Generic; 17 | using Azure.Search.Documents.Models; 18 | using System.Linq; 19 | using Azure.Storage.Blobs; 20 | using Azure.Storage.Blobs.Models; 21 | using Microsoft.Azure.CognitiveServices.Knowledge.QnAMaker; 22 | using Microsoft.Azure.CognitiveServices.Knowledge.QnAMaker.Models; 23 | 24 | namespace QnAIntegrationCustomSkill 25 | { 26 | public static class Search 27 | { 28 | private static string searchApiKey = Environment.GetEnvironmentVariable("SearchServiceApiKey", EnvironmentVariableTarget.Process); 29 | private static string searchServiceName = Environment.GetEnvironmentVariable("SearchServiceName", EnvironmentVariableTarget.Process); 30 | private static string searchIndexName = Constants.indexName; 31 | 32 | private static string storageAccountName = Environment.GetEnvironmentVariable("StorageAccountName", EnvironmentVariableTarget.Process); 33 | private static string storageAccountKey = Environment.GetEnvironmentVariable("StorageAccountKey", EnvironmentVariableTarget.Process); 34 | 35 | private static BlobServiceClient blobServiceClient = new BlobServiceClient(Environment.GetEnvironmentVariable("AzureWebJobsStorage", EnvironmentVariableTarget.Process)); 36 | 37 | // Create a SearchIndexClient to send create/delete index commands 38 | private static Uri serviceEndpoint = new Uri($"https://{searchServiceName}.search.windows.net/"); 39 | private static AzureKeyCredential credential = new AzureKeyCredential(searchApiKey); 40 | 41 | // Create a SearchClient to load and query documents 42 | private static SearchClient searchClient = new SearchClient(serviceEndpoint, searchIndexName, credential); 43 | 44 | private static QnAMakerRuntimeClient runtimeClient; 45 | private static string qnaMakerEndpoint = Environment.GetEnvironmentVariable("QnAMakerEndpoint", EnvironmentVariableTarget.Process); 46 | private static string blobBaseURL = $"https://{storageAccountName}.blob.core.windows.net/{Constants.containerName}/"; 47 | 48 | [FunctionName("Search")] 49 | public static async Task Run( 50 | [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, 51 | ILogger log) 52 | { 53 | log.LogInformation("C# HTTP trigger function processed a request."); 54 | 55 | string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); 56 | SearchRequest data = JsonConvert.DeserializeObject(requestBody); 57 | 58 | string kbId; 59 | string qnaRuntimeKey; 60 | QnASearchResultList qnaResponse = null; 61 | if (data.getAnswer) 62 | { 63 | 64 | try 65 | { 66 | kbId = await GetKbId(log); 67 | qnaRuntimeKey = await GetRuntimeKey(log); 68 | runtimeClient = new QnAMakerRuntimeClient(new EndpointKeyServiceClientCredentials(qnaRuntimeKey)) 69 | { 70 | RuntimeEndpoint = qnaMakerEndpoint 71 | }; 72 | 73 | var qnaOptions = new QueryDTO 74 | { 75 | Question = data.q, 76 | Top = 1, 77 | ScoreThreshold = 30 78 | }; 79 | qnaResponse = await runtimeClient.Runtime.GenerateAnswerAsync(kbId, qnaOptions); 80 | } 81 | catch (Exception e) 82 | { 83 | log.LogInformation(e.Message); 84 | } 85 | 86 | } 87 | 88 | 89 | SearchOptions options = new SearchOptions() 90 | { 91 | Size = data.top, 92 | Skip = data.skip, 93 | IncludeTotalCount = true, 94 | Filter = CreateFilterExpression(data.filters) 95 | }; 96 | 97 | options.Facets.Add("keyPhrases"); 98 | options.Facets.Add("fileType"); 99 | options.HighlightFields.Add("content"); 100 | options.Select.Add("metadata_storage_name"); 101 | options.Select.Add("metadata_storage_path"); 102 | options.Select.Add("id"); 103 | 104 | var response = await searchClient.SearchAsync(RemoveStopwords(data.q), options); 105 | 106 | Dictionary> facets = new Dictionary>(); 107 | 108 | foreach (KeyValuePair> facet in response.Value.Facets) 109 | { 110 | //KeyValuePair> f = new KeyValuePair>(facet.Key, new List()); 111 | 112 | var values = new List(); 113 | foreach (FacetResult result in facet.Value) 114 | { 115 | FacetValue value = new FacetValue() { count = result.Count, value = result.Value.ToString() }; 116 | values.Add(value); 117 | } 118 | 119 | facets[facet.Key] = values; 120 | } 121 | 122 | SearchOutput output = new SearchOutput(); 123 | output.count = response.Value.TotalCount; 124 | output.results = response.Value.GetResults().ToList(); 125 | output.facets = facets; 126 | 127 | if (qnaResponse != null) 128 | { 129 | output.answers = new QnAResult(); 130 | output.answers.answer = qnaResponse.Answers.First(); 131 | var source = output.answers.answer.Source; 132 | output.answers.document = await GetDocument(source, output.results, log); 133 | } 134 | 135 | return new OkObjectResult(output); 136 | } 137 | 138 | public static string RemoveStopwords(string query) 139 | { 140 | List stopWords = new List(){"a", "about", "above", "after", "again", "against", "all", "am", "an", "and", "any", "are", "aren't", "as", "at", "be", "because", "been", "before", "being", "below", "between", "both", "but", "by", "can't", "cannot", "could", "couldn't", "did", 141 | "didn't", "do", "does", "doesn't", "doing", "don't", "down", "during", "each", "few", "for", "from", "further", "had", "hadn't", "has", "hasn't", "have", "haven't", "having", "he", "he'd", "he'll", "he's", "her", "here", "here's", "hers", "herself", 142 | "him", "himself", "his", "how", "how's", "i", "i'd", "i'll", "i'm", "i've", "if", "in", "into", "is", "isn't", "it", "it's", "its", "itself", "let's", "me", "more", "most", "mustn't", "my", "myself", "no", "nor", "not", "of", "off", "on", "once", "only", 143 | "or", "other", "ought", "our", "ours", "ourselves", "out", "over", "own", "same", "shan't", "she", "she'd", "she'll", "she's", "should", "shouldn't", "so", "some", "such", "than", "that", "that's", "the", "their", "theirs", "them", "themselves", "then", 144 | "there", "there's", "these", "they", "they'd", "they'll", "they're", "they've", "this", "those", "through", "to", "too", "under", "until", "up", "very", "was", "wasn't", "we", "we'd", "we'll", "we're", "we've", "were", "weren't", "what", "what's", "when", 145 | "when's", "where", "where's", "which", "while", "who", "who's", "whom", "why", "why's", "with", "won't", "would", "wouldn't", "you", "you'd", "you'll", "you're", "you've", "your", "yours", "yourself", "yourselves" }; 146 | 147 | var words = query.ToLower().Split(' '); 148 | 149 | List outputs = new List(); 150 | foreach (string word in words) 151 | { 152 | if (!stopWords.Contains(word)) 153 | { 154 | outputs.Add(word); 155 | } 156 | } 157 | 158 | return string.Join(" ", outputs); 159 | } 160 | 161 | public static string CreateFilterExpression(List filters) 162 | { 163 | if (filters == null || filters.Count <= 0) 164 | { 165 | return null; 166 | } 167 | 168 | List filterExpressions = new List(); 169 | 170 | List keyPhraseFilters = filters.Where(f => f.field == "keyPhrases").ToList(); 171 | List fileTypeFilters = filters.Where(f => f.field == "fileType").ToList(); 172 | 173 | List keyPhraseFilterValues = keyPhraseFilters.Select(f => f.value).ToList(); 174 | 175 | if (keyPhraseFilterValues.Count > 0) 176 | { 177 | string filterStr = string.Join(",", keyPhraseFilterValues); 178 | filterExpressions.Add($"{"keyPhrases"}/any(t: search.in(t, '{filterStr}', ','))"); 179 | } 180 | 181 | List fileTypeFilterValues = fileTypeFilters.Select(f => f.value).ToList(); 182 | foreach (var value in fileTypeFilterValues) 183 | { 184 | filterExpressions.Add($"fileType eq '{value}'"); 185 | } 186 | 187 | return string.Join(" and ", filterExpressions); 188 | } 189 | 190 | private static async Task GetKbId(ILogger log) 191 | { 192 | string kbID = string.Empty; 193 | var path = Path.Join(Environment.GetEnvironmentVariable("HOME", EnvironmentVariableTarget.Process), Constants.qnamakerFolderPath); 194 | var filePath = Path.Join(path, Constants.kbIdBlobName + ".txt"); 195 | // Check for kbid in local file system 196 | if (File.Exists(filePath)) 197 | { 198 | kbID = File.ReadAllText(filePath); 199 | } 200 | else 201 | { 202 | BlobContainerClient containerClient = blobServiceClient.GetBlobContainerClient(Constants.kbContainerName); 203 | BlobClient kbidBlobClient = containerClient.GetBlobClient(Constants.kbIdBlobName); 204 | // Check blob for kbid 205 | if (await kbidBlobClient.ExistsAsync()) 206 | { 207 | BlobDownloadInfo download = await kbidBlobClient.DownloadAsync(); 208 | using (var streamReader = new StreamReader(download.Content)) 209 | { 210 | while (!streamReader.EndOfStream) 211 | { 212 | kbID = await streamReader.ReadLineAsync(); 213 | } 214 | } 215 | } 216 | 217 | } 218 | return kbID; 219 | } 220 | 221 | private static async Task GetRuntimeKey(ILogger log) 222 | { 223 | string runtimeKey = string.Empty; 224 | var path = Path.Join(Environment.GetEnvironmentVariable("HOME", EnvironmentVariableTarget.Process), Constants.qnamakerFolderPath); 225 | var filePath = Path.Join(path, Constants.keyBlobName + ".txt"); 226 | // Check for kbid in local file system 227 | if (File.Exists(filePath)) 228 | { 229 | runtimeKey = File.ReadAllText(filePath); 230 | } 231 | else 232 | { 233 | BlobContainerClient containerClient = blobServiceClient.GetBlobContainerClient(Constants.kbContainerName); 234 | BlobClient keyBlobClient = containerClient.GetBlobClient(Constants.keyBlobName); 235 | // Check blob for kbid 236 | if (await keyBlobClient.ExistsAsync()) 237 | { 238 | BlobDownloadInfo download = await keyBlobClient.DownloadAsync(); 239 | using (var streamReader = new StreamReader(download.Content)) 240 | { 241 | while (!streamReader.EndOfStream) 242 | { 243 | runtimeKey = await streamReader.ReadLineAsync(); 244 | } 245 | } 246 | } 247 | 248 | } 249 | return runtimeKey; 250 | } 251 | 252 | private static async Task> GetDocument(string source, List> searchResult, ILogger log) 253 | { 254 | if (source == null) 255 | { 256 | return null; 257 | } 258 | 259 | var blobURL = string.Concat(blobBaseURL, source); 260 | log.LogInformation(blobURL); 261 | // check if source present in search results 262 | foreach (var doc in searchResult) 263 | { 264 | object name; 265 | if (doc.Document.TryGetValue("metadata_storage_path", out name) && name.ToString() == blobURL) 266 | { 267 | log.LogInformation("blob path constructed: " + blobURL + " blob path in index: " + name.ToString()); 268 | return doc; 269 | } 270 | } 271 | 272 | // else query search index 273 | SearchOptions options = new SearchOptions(); 274 | options.SearchFields.Add("metadata_storage_path"); 275 | var result = await searchClient.SearchAsync("\""+blobURL+"\"", options); 276 | return result.Value.GetResults().ToList().First(); 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /CustomSkillForDataIngestion/QnAIntegrationCustomSkill/Suggest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | using System; 5 | using System.IO; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.Azure.WebJobs; 9 | using Microsoft.Azure.WebJobs.Extensions.Http; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.Extensions.Logging; 12 | using Newtonsoft.Json; 13 | using Common; 14 | using Azure; 15 | using Azure.Search.Documents; 16 | using System.Collections.Generic; 17 | using Azure.Search.Documents.Models; 18 | using System.Linq; 19 | 20 | namespace QnAIntegrationCustomSkill 21 | { 22 | public static class Suggest 23 | { 24 | private static string searchApiKey = Environment.GetEnvironmentVariable("SearchServiceApiKey", EnvironmentVariableTarget.Process); 25 | private static string searchServiceName = Environment.GetEnvironmentVariable("SearchServiceName", EnvironmentVariableTarget.Process); 26 | private static string searchIndexName = Constants.indexName; 27 | 28 | // Create a SearchIndexClient to send create/delete index commands 29 | private static Uri serviceEndpoint = new Uri($"https://{searchServiceName}.search.windows.net/"); 30 | private static AzureKeyCredential credential = new AzureKeyCredential(searchApiKey); 31 | 32 | // Create a SearchClient to load and query documents 33 | private static SearchClient searchClient = new SearchClient(serviceEndpoint, searchIndexName, credential); 34 | 35 | [FunctionName("Suggest")] 36 | public static async Task Run( 37 | [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, 38 | ILogger log) 39 | { 40 | log.LogInformation("C# HTTP trigger function processed a request."); 41 | 42 | string q = req.Query["q"]; 43 | string top = req.Query["top"]; 44 | string suggester = req.Query["suggester"]; 45 | 46 | string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); 47 | dynamic data = JsonConvert.DeserializeObject(requestBody); 48 | q = q ?? data?.q; 49 | top = top ?? data?.top; 50 | suggester = suggester ?? data?.suggester; 51 | 52 | AutocompleteOptions options = new AutocompleteOptions() 53 | { 54 | Size = int.Parse(top) 55 | }; 56 | 57 | var response = await searchClient.AutocompleteAsync(q, suggester, options); 58 | 59 | var output = new Dictionary>(); 60 | 61 | output["suggestions"] = response.Value.Results.ToList(); 62 | return new OkObjectResult(output); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /CustomSkillForDataIngestion/QnAIntegrationCustomSkill/UploadDocument.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | using System; 5 | using System.IO; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.Azure.WebJobs; 9 | using Microsoft.Azure.WebJobs.Extensions.Http; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.Extensions.Logging; 12 | using Newtonsoft.Json; 13 | using Common; 14 | using Azure.Search.Documents.Indexes; 15 | using Azure; 16 | using Azure.Search.Documents; 17 | using System.Collections.Generic; 18 | using Microsoft.Extensions.Options; 19 | using Azure.Search.Documents.Models; 20 | using System.Linq; 21 | using Azure.Storage.Blobs; 22 | using Azure.Storage; 23 | using Azure.Storage.Sas; 24 | using System.Net; 25 | using System.Net.Http; 26 | using Azure.Storage.Blobs.Models; 27 | 28 | namespace QnAIntegrationCustomSkill 29 | { 30 | public static class UploadDocument 31 | { 32 | private static BlobServiceClient blobServiceClient = new BlobServiceClient(Environment.GetEnvironmentVariable("AzureWebJobsStorage", EnvironmentVariableTarget.Process)); 33 | private static BlobContainerClient containerClient = blobServiceClient.GetBlobContainerClient(Constants.containerName); 34 | 35 | [FunctionName("Upload")] 36 | public static async Task Run( 37 | [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, 38 | ILogger log) 39 | { 40 | log.LogInformation("C# HTTP trigger function processed a request."); 41 | 42 | string fileName = req.Query["name"]; 43 | string file = req.Query["file"]; 44 | string fileType = req.Query["fileType"]; 45 | 46 | string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); 47 | dynamic data = JsonConvert.DeserializeObject(requestBody); 48 | fileName = fileName ?? data?.name; 49 | file = file ?? data?.file; 50 | fileType = fileType ?? data?.fileType; 51 | 52 | var bytes = Convert.FromBase64String(file); 53 | var contents = new MemoryStream(bytes); 54 | 55 | BlobClient blobClient = containerClient.GetBlobClient(fileName); 56 | var response = await blobClient.UploadAsync(contents, new BlobHttpHeaders { ContentType = fileType }); 57 | 58 | return new OkObjectResult(response.Value); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /CustomSkillForDataIngestion/QnAIntegrationCustomSkill/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "functionTimeout": "00:35:00", 4 | "extensions": { 5 | "queues": { 6 | "batchSize": 1, 7 | "maxDequeueCount": 1 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /CustomSkillForDataIngestion/README.md: -------------------------------------------------------------------------------- 1 | # A single endpoint for Search + QnA 2 | 3 | A single endpoint can be created to call both Azure Cognitive Search and QnA Maker. This document describes the input and output of the `/api/search` endpoint that serves as a single endpoint. 4 | 5 | ## Inputs 6 | 7 | ```json 8 | { 9 | "q": "how do mrna vaccines work?", 10 | "top": 10, 11 | "skip": 0, 12 | "filters": [], 13 | "getAnswer": true 14 | } 15 | ``` 16 | 17 | Note: in the current setup, facet to include in the query are pulled from the Azure Function App Settings so they are not included as an input here. 18 | 19 | ## Outputs 20 | 21 | Below is a sample output from the call above. The output has been abbreviated for readability. 22 | 23 | ```json 24 | { 25 | "count":11, 26 | "results":[ 27 | { 28 | "score":7.169414, 29 | "highlights":{ 30 | "content":[ 31 | "For more information: www.cdc.gov/COVID19\n\nWhat Clinicians Need to Know About the \n\nPfizer-BioNTech COVID-19 Vaccine\n\nAmanda Cohn, MD\n\nSarah Mbaeyi, MD, MPH\n\nDecember 13, 2020\n\n\n\n2\n\nPfizer-BioNTech COVID-19 Vaccine\n\n\n\n Lipid nanoparticle-formulated mRNA vaccine \n\nencoding the spike protein\n\n– Spike protein: facilitates entry of virus into cells\n\n Vaccination induces antibodies that can block entry \n\nof SARS-CoV-2 into cells, thereby preventing \n\ninfection\n\n FDA issued an Emergency Use Authorization on \n\nDecember 13, 2020 for use in persons aged ≥16 \n\nyears\n\nPfizer-BioNTech COVID-19 vaccine\n\nSpike protein\n\n3\n\n\n\n mRNA vaccines take advantage of the process that cells use to make proteins in \n\norder to trigger an immune response \n\n– Like all vaccines, COVID-19 mRNA vaccines have been rigorously tested for \n\nsafety before being authorized for use in the United States\n\n– mRNA technology is new, but not unknown." 32 | ] 33 | }, 34 | "document":{ 35 | "metadata_storage_path":"https://q2storage7nzzkuzdcevvy.blob.core.windows.net/qna-container/pfizer-biontech-vaccine-what-Clinicians-need-to-know.pdf", 36 | "id":"aHR0cHM6Ly9xMnN0b3JhZ2U3bnp6a3V6ZGNldnZ5LmJsb2IuY29yZS53aW5kb3dzLm5ldC9xbmEtY29udGFpbmVyL3BmaXplci1iaW9udGVjaC12YWNjaW5lLXdoYXQtQ2xpbmljaWFucy1uZWVkLXRvLWtub3cucGRm0", 37 | "metadata_storage_name":"pfizer-biontech-vaccine-what-Clinicians-need-to-know.pdf" 38 | } 39 | } 40 | ... 41 | ... 42 | ], 43 | "facets":{ 44 | "keyPhrases":[ 45 | { 46 | "value":"COVID", 47 | "count":8 48 | }, 49 | { 50 | "value":"Asked Questions", 51 | "count":5 52 | }, 53 | { 54 | "value":"CDC", 55 | "count":5 56 | } 57 | ], 58 | "fileType":[ 59 | { 60 | "value":".pdf", 61 | "count":8 62 | }, 63 | { 64 | "value":".docx", 65 | "count":3 66 | } 67 | ] 68 | }, 69 | "answers":{ 70 | "answer":{ 71 | "questions":[ 72 | "How do the Pfizer and Moderna mRNA vaccines work?" 73 | ], 74 | "answer":"The vaccines contain synthetic mRNA, which is genetic information used to make the SARS-CoV-2 spike protein. The spike protein is the part of the virus that attaches to human cells. The spike protein alone cannot cause COVID-19. Once the spike protein is created it causes the immune system to make antibodies against the virus. These antibodies can the provide protection if a person comes into contact with the virus.", 75 | "score":95.0, 76 | "id":19, 77 | "source":"COVID-19 Vaccine FAQ.pdf", 78 | "metadata":[ 79 | 80 | ], 81 | "context":{ 82 | "isContextOnly":false, 83 | "prompts":[ 84 | 85 | ] 86 | } 87 | }, 88 | "document":{ 89 | "score":4.249519, 90 | "highlights":{ 91 | "content":[ 92 | "How do the Pfizer and Moderna mRNA vaccines work?", 93 | "The vaccines contain synthetic mRNA, which is genetic information used to make the SARS-CoV-2 spike protein.", 94 | ] 95 | }, 96 | "document":{ 97 | "metadata_storage_path":"https://q2storage7nzzkuzdcevvy.blob.core.windows.net/qna-container/COVID-19%20Vaccine%20FAQ.pdf", 98 | "id":"aHR0cHM6Ly9xMnN0b3JhZ2U3bnp6a3V6ZGNldnZ5LmJsb2IuY29yZS53aW5kb3dzLm5ldC9xbmEtY29udGFpbmVyL0NPVklELTE5JTIwVmFjY2luZSUyMEZBUS5wZGY1", 99 | "metadata_storage_name":"COVID-19 Vaccine FAQ.pdf" 100 | } 101 | } 102 | } 103 | } 104 | ``` -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | Cognitive Search Question Answering Solution Accelerator 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cognitive Search Question Answering Solution Accelerator 2 | 3 | An integrated search solution leveraging [Azure Cognitive Search](https://azure.microsoft.com/services/search/) and [QnA Maker](https://www.qnamaker.ai/) to provide instant answers to common questions. Learn more about the repo in this [blog post](https://techcommunity.microsoft.com/t5/azure-ai/qna-with-azure-cognitive-search/ba-p/2081381). 4 | 5 | ![Screenshot of sample web app](./images/web-app.png) 6 | 7 | ## Features 8 | 9 | This solution accelerator leverages the power of Azure Cognitive Search together with QnA Maker to find answers to your questions in a similar way to how Bing and Google suggest relevant answers to queries. 10 | 11 | Ordinarily, Azure Cognitive Search returns the most relevant documents for your search query but together with QnA Maker integration, it can not only find the most relevant documents but also pull questions and answers out of the document and suggest the most relevant answers. During Cognitive Search data ingestion, a custom skill sends the document to QnA Maker for processing. 12 | 13 | Please note that not all documents support the [question/answer format required by QnA Maker](https://docs.microsoft.com/azure/cognitive-services/qnamaker/concepts/data-sources-and-content#file-and-url-data-types). By default, the logic in the Search service indexer also ingests only the following file types: `.pdf,.docx,.doc,.xlsx,.xls,.html,.rtf,.txt,.tsv`. You can change this by modifying the `indexedFileNameExtensions` property in the [Indexer.json](./CustomSkillForDataIngestion/QnAIntegrationCustomSkill/Assets/Indexer.json). 14 | 15 | This solution accelerator contains the following artifacts: 16 | + ARM template to set up the solution 17 | + Custom skill in Cognitive Search, which ingests the data into QnA Maker 18 | + User interface to view the results 19 | 20 | ![Cognitive Search QnA Maker Solution Architecture](./images/CogSearchQnAMakerArchitecture.jpg) 21 | 22 | ## Demo 23 | 24 | You can view a live demo of this repo at the following link: 25 | 26 | [https://qna-ui-i3iohrgwgujpo.azurewebsites.net/](https://qna-ui-i3iohrgwgujpo.azurewebsites.net/) 27 | 28 | ## Getting Started 29 | 30 | ### Prerequisites 31 | 32 | + A GitHub account 33 | + [Node.js and Git](https://nodejs.org/) 34 | + [Visual Studio Code](https://code.visualstudio.com) installed 35 | + [Postman](https://www.getpostman.com/) for making API calls 36 | 37 | ### 1. Deploy Resources 38 | 39 | The services and components needed for the solution are packaged in the repo's [ARM template](./azuredeploy.json). Click the **Deploy to Azure** button to get started: 40 | 41 | [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure-Samples%2Fsearch-qna-maker-accelerator%2Fmain%2Fazuredeploy.json) 42 | 43 | If you want to use the latest version of QnA Maker: [QnA Maker managed](https://techcommunity.microsoft.com/t5/azure-ai/introducing-qna-maker-managed-now-in-public-preview/ba-p/1845575) (currently in public preview) which includes new features such as [precise answering](https://docs.microsoft.com/azure/cognitive-services/qnamaker/reference-precise-answering), click the **Deploy to Azure** button given below: 44 | 45 | [![Deploy to Azure QnA Maker Managed](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure-Samples%2Fsearch-qna-maker-accelerator%2FQnAMakerV2PublicPreview%2Fazuredeploy.json) 46 | 47 | The following resources will be deployed: 48 | 49 | 1. Azure Cognitive Search 50 | 2. QnA Maker Cognitive Service (this will always be deployed to the West US region, but your data is not stored here - see [here](https://docs.microsoft.com/azure/cognitive-services/qnamaker/concepts/azure-resources?tabs=v1#management-service-region)) 51 | 3. Azure App Service, App Service Plan 52 | 4. Azure App Service, Website for Qna Maker 53 | 5. Azure App Service, Website to host the UI 54 | 6. Storage Account 55 | 7. Azure Function App 56 | 8. Cognitive Services All-in-one resource 57 | 9. Application Insights 58 | 59 | The deployment may take several minutes. Once the deployment finishes, navigate over to the **Outputs** tab: 60 | 61 | ![Deployment screenshot](./images/deployment.png) 62 | 63 | Copy the value of the HTTP Trigger. You'll use this value in the next step. 64 | 65 | ![URL to copy](./images/qna-copy-url.png) 66 | 67 | 68 | ### 2. Initialize the solution 69 | 70 | Open up a new browser tab and paste the URL into the browser. This will run for about a minute and then you'll see a message indicating success or failure. 71 | 72 | ![Initialize solution accelerator](./images/initialize-accelerator.png) 73 | 74 | ### 3. Upload documents 75 | 76 | Navigate to the storage account in the resource group you just created. Find the container named `qna-container` and upload your documents into the container. 77 | 78 | Sample documents are available in the [SampleDocuments](./SampleDocuments) folder. 79 | 80 | ___ 81 | > NOTE: if you would prefer to pull data from a different, pre-existing blob storage account, you may instead change the data source in your Search service to point to a container in a different blob storage account. Just change the connection string and container name in the data source in the Search service. 82 | ___ 83 | 84 | ### 4. Go to the UI 85 | 86 | As part of the ARM template, a UI is deployed to `https://{prefix}-ui-{randomString}.azurewebsites.net` similar to the screenshot below. Keep in mind it will take a few minutes for the documents to become available in the search index after adding them to storage. 87 | 88 | ![Screenshot of sample web app](./images/search-results.png) 89 | 90 | ___ 91 | > NOTE: At this point, you should consider [adding authentication to the web app in the Azure portal](https://docs.microsoft.com/azure/app-service/scenario-secure-app-authentication-app-service) to secure your data. 92 | ___ 93 | 94 | The UI is a React-based Web App available in the `SearchUI` folder. Navigate to [**SearchUI/README.md**](SearchUI/README.md) for more details on the web app. 95 | 96 | ## Frequently Asked Questions (FAQ) 97 | **How is this solution utilizing QnaMaker different from the [new semantic search functionality](https://docs.microsoft.com/azure/search/semantic-search-overview) in Cognitive Search?** 98 | 99 | The semantic search question answering does have a functional overlap with this solution accelerator using QnA Maker. Here's what you need to know. 100 | + The QnA extracted by QnA Maker in this solution accelerator are high precision. This means that this type of QnA is valuable for scenarios where there's a set of questions users frequently ask and you want to be able to provide a precise answer. These QnAs can also be edited in the qnamaker.ai portal, giving you full control over the answers. 101 | + The QnAs provided by the semantic search feature are high recall. This gives you the ability to get meaningful answers to much more specific questions. These are extracted at runtime and non-customizable. 102 | 103 | However, the two options are not mutually exclusive and can be used together. The recommendation to customers is to use the QnA Maker solution for the head queries (high precision, curated), and fallback to the semantic search QnA when there is no good match in QnA Maker. Specifically: 104 | + Using the accelerator solution, both the QnA Maker and Semantic answer are available simultaneously. 105 | + Set an appropriate threshold for the QnA Maker Answer. 106 | + If QnA Maker returns the answer, show that in the final result (ignoring the Semantic Search answer). 107 | + If not, show the Semantic Search answer if available. 108 | + This will need code changes in the final merging logic of the results. 109 | 110 | 111 | ## Disclaimer 112 | This repo has been tested with document sets as big as 2,000 documents. If you have a larger dataset or run into any issues with this repo, please open an [issue](https://github.com/Azure-Samples/search-qna-maker-accelerator/issues) and we will get back to you. 113 | 114 | ## Resources 115 | + [Cognitive Search Documentation](https://docs.microsoft.com/azure/search/) 116 | + [QnA Maker Documentation](https://docs.microsoft.com/azure/cognitive-services/QnAMaker/) 117 | 118 | ## Legal 119 | + [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct) 120 | + [Trademark notice](https://docs.opensource.microsoft.com/content/releasing/index.html) 121 | + [Security reporting instructions](https://docs.opensource.microsoft.com/content/releasing/security.html) 122 | -------------------------------------------------------------------------------- /SampleDocuments/AI enrichment concepts.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/SampleDocuments/AI enrichment concepts.pdf -------------------------------------------------------------------------------- /SampleDocuments/Analyzers for linguistic and text processing - Azure Cognitive Search _ Microsoft Docs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/SampleDocuments/Analyzers for linguistic and text processing - Azure Cognitive Search _ Microsoft Docs.pdf -------------------------------------------------------------------------------- /SampleDocuments/Choose a pricing tier - Azure Cognitive Search _ Microsoft Docs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/SampleDocuments/Choose a pricing tier - Azure Cognitive Search _ Microsoft Docs.pdf -------------------------------------------------------------------------------- /SampleDocuments/Create a suggester - Azure Cognitive Search _ Microsoft Docs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/SampleDocuments/Create a suggester - Azure Cognitive Search _ Microsoft Docs.pdf -------------------------------------------------------------------------------- /SampleDocuments/Filter on search results - Azure Cognitive Search _ Microsoft Docs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/SampleDocuments/Filter on search results - Azure Cognitive Search _ Microsoft Docs.pdf -------------------------------------------------------------------------------- /SampleDocuments/Fuzzy search - Azure Cognitive Search _ Microsoft Docs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/SampleDocuments/Fuzzy search - Azure Cognitive Search _ Microsoft Docs.pdf -------------------------------------------------------------------------------- /SampleDocuments/How to model complex data types - Azure Cognitive Search _ Microsoft Docs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/SampleDocuments/How to model complex data types - Azure Cognitive Search _ Microsoft Docs.pdf -------------------------------------------------------------------------------- /SampleDocuments/How to work with search results - Azure Cognitive Search _ Microsoft Docs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/SampleDocuments/How to work with search results - Azure Cognitive Search _ Microsoft Docs.pdf -------------------------------------------------------------------------------- /SampleDocuments/Introduction to Azure Cognitive Search - Azure Cognitive Search _ Microsoft Docs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/SampleDocuments/Introduction to Azure Cognitive Search - Azure Cognitive Search _ Microsoft Docs.pdf -------------------------------------------------------------------------------- /SampleDocuments/Monitor operations and activity - Azure Cognitive Search _ Microsoft Docs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/SampleDocuments/Monitor operations and activity - Azure Cognitive Search _ Microsoft Docs.pdf -------------------------------------------------------------------------------- /SampleDocuments/Security overview - Azure Cognitive Search _ Microsoft Docs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/SampleDocuments/Security overview - Azure Cognitive Search _ Microsoft Docs.pdf -------------------------------------------------------------------------------- /SampleDocuments/Service limits for tiers and skus - Azure Cognitive Search _ Microsoft Docs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/SampleDocuments/Service limits for tiers and skus - Azure Cognitive Search _ Microsoft Docs.pdf -------------------------------------------------------------------------------- /SampleDocuments/Simple query syntax - Azure Cognitive Search _ Microsoft Docs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/SampleDocuments/Simple query syntax - Azure Cognitive Search _ Microsoft Docs.pdf -------------------------------------------------------------------------------- /SampleDocuments/Skillset concepts and workflow - Azure Cognitive Search _ Microsoft Docs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/SampleDocuments/Skillset concepts and workflow - Azure Cognitive Search _ Microsoft Docs.pdf -------------------------------------------------------------------------------- /SearchUI/.env: -------------------------------------------------------------------------------- 1 | REACT_APP_FUNCTION_URL=http://localhost:7071 2 | REACT_APP_FUNCTION_CODE=none -------------------------------------------------------------------------------- /SearchUI/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /SearchUI/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /SearchUI/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to Node Functions", 6 | "type": "node", 7 | "request": "attach", 8 | "port": 9229, 9 | "preLaunchTask": "func: host start" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /SearchUI/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.deploySubpath": "api", 3 | "azureFunctions.postDeployTask": "npm install", 4 | "azureFunctions.projectLanguage": "JavaScript", 5 | "azureFunctions.projectRuntime": "~2", 6 | "debug.internalConsoleOptions": "neverOpen", 7 | "azureFunctions.preDeployTask": "npm prune" 8 | } 9 | -------------------------------------------------------------------------------- /SearchUI/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "func", 6 | "command": "host start", 7 | "problemMatcher": "$func-watch", 8 | "isBackground": true, 9 | "dependsOn": "npm install", 10 | "options": { 11 | "cwd": "${workspaceFolder}/api" 12 | } 13 | }, 14 | { 15 | "type": "shell", 16 | "label": "npm install", 17 | "command": "npm install", 18 | "options": { 19 | "cwd": "${workspaceFolder}/api" 20 | } 21 | }, 22 | { 23 | "type": "shell", 24 | "label": "npm prune", 25 | "command": "npm prune --production", 26 | "problemMatcher": [], 27 | "options": { 28 | "cwd": "${workspaceFolder}/api" 29 | } 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /SearchUI/DeploymentInfo.md: -------------------------------------------------------------------------------- 1 | # Deployment Info 2 | 3 | This web app is currently deployed from the ARM template using MSDeploy. A zip folder is needed for this deployment method. 4 | 5 | To create the zip folder: 6 | 7 | 1. Navigate to the `SearchUI` folder 8 | 9 | ``` 10 | cd SearchUI 11 | ``` 12 | 13 | 2. Install the required packages 14 | 15 | ``` 16 | npm install 17 | ``` 18 | 19 | 3. Build the web app 20 | 21 | ``` 22 | npm run-script build 23 | ``` 24 | 25 | 4. Next, create a zip file from the contents of the `build` folder. It's important that the contents of the folder are at the top level of the zip file. To do this in File Explorer, navigate to the build folder, select every item, then right-click and send the contents to a zip file. -------------------------------------------------------------------------------- /SearchUI/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | Cognitive Search Question Answering Solution Accelerator 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /SearchUI/README.md: -------------------------------------------------------------------------------- 1 | # Azure Cognitive Search UI 2 | 3 | This sample is a React template for [Azure Cognitive Search](https://docs.microsoft.com/en-us/azure/search/search-what-is-azure-search). It leverages Azure Functions as the backend to communicate with Azure Cognitive Search and QnA Maker. 4 | 5 | ![Screenshot of sample web app](./images/web-app.png) 6 | 7 | ## Running the application locally 8 | 9 | To run the sample locally, follow the steps below. 10 | 11 | ### Prerequisites 12 | 13 | - [Node.js and Git](https://nodejs.org/) 14 | - [Visual Studio](https://visualstudio.microsoft.com/) and [Function tools](https://docs.microsoft.com/en-us/azure/azure-functions/functions-develop-vs) for Visual Studio 15 | - [Visual Studio Code](https://code.visualstudio.com/?WT.mc_id=shopathome-github-jopapa) 16 | 17 | ### Setup 18 | 19 | 1. Clone (or Fork and Clone) this repository 20 | 21 | 1. Open the Azure Function project `CustomSkillForDataIngestion\CustomSkillForDataIngestion.sln` in Visual Studio 22 | 23 | 1. Add a `local.settings.json` file to the project 24 | 25 | The `local.settings.json` file holds all of the keys that the application needs and should include the following json: 26 | 27 | ```json 28 | { 29 | "IsEncrypted": false, 30 | "Values": { 31 | "FUNCTIONS_WORKER_RUNTIME": "dotnet", 32 | 33 | "AzureWebJobsStorage": "DefaultEndpointsProtocol=https;AccountName={StorageAcountName};AccountKey={StorageAccountKey};", 34 | "StorageAccountName": "", 35 | "StorageAccountKey": "", 36 | 37 | "SearchServiceApiKey": "", 38 | "SearchServiceName": "", 39 | 40 | "QnAMakerEndpoint": "", 41 | 42 | }, 43 | "Host": { 44 | "CORS": "*" 45 | } 46 | } 47 | ``` 48 | 49 | ### Run the Azure functions 50 | 51 | 1. Run the Azure functon project in visual studio 52 | 53 | ### Running the front-end 54 | 55 | 1. Install front-end dependencies... 56 | 57 | ```bash 58 | npm install 59 | ``` 60 | 61 | 1. Run the front-end project in the browser (automatically opens a browser window). 62 | 63 | ```bash 64 | npm start 65 | ``` 66 | 67 | The react project will pull the URL for the Azure functions from the `.env` file. If you'd like to run the front-end locally with the deployed Azure Functions, update the credentials in the `.env` file. -------------------------------------------------------------------------------- /SearchUI/images/basic-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/SearchUI/images/basic-arch.png -------------------------------------------------------------------------------- /SearchUI/images/web-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/SearchUI/images/web-app.png -------------------------------------------------------------------------------- /SearchUI/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample", 3 | "version": "0.1.0", 4 | "private": true, 5 | "engines": { 6 | "node": ">=10" 7 | }, 8 | "dependencies": { 9 | "@material-ui/core": "^4.11.0", 10 | "@material-ui/icons": "^4.9.1", 11 | "@material-ui/lab": "^4.0.0-alpha.56", 12 | "@testing-library/jest-dom": "^4.2.4", 13 | "@testing-library/react": "^9.3.2", 14 | "@testing-library/user-event": "^7.1.2", 15 | "axios": "^0.21.1", 16 | "bootstrap": "^4.5.2", 17 | "popper.js": "^1.16.1", 18 | "react": "^16.13.1", 19 | "react-dom": "^16.13.1", 20 | "react-html-parser": "^2.0.2", 21 | "react-markdown": "^5.0.2", 22 | "react-router-dom": "^5.2.0", 23 | "react-scripts": "^4.0.1", 24 | "styled-components": "^5.2.0" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test", 30 | "eject": "react-scripts eject" 31 | }, 32 | "proxy": "http://localhost:7071", 33 | "eslintConfig": { 34 | "extends": "react-app" 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /SearchUI/public/config.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | var http = require('http'); 5 | var server = http.createServer((request, response) => { 6 | 7 | let variables = { 8 | "code": process.env.REACT_APP_FUNCTION_CODE, 9 | "url": process.env.REACT_APP_FUNCTION_URL 10 | }; 11 | response.writeHead(200, {'Content-Type': 'application/json'}); 12 | response.end(JSON.stringify(variables)); 13 | }); 14 | server.listen(process.env.PORT || 3000); 15 | -------------------------------------------------------------------------------- /SearchUI/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/SearchUI/public/favicon.ico -------------------------------------------------------------------------------- /SearchUI/public/iisnode.yml: -------------------------------------------------------------------------------- 1 | nodeProcessCommandLine: "D:\Program Files (x86)\nodejs\14.15.0\node.exe" -------------------------------------------------------------------------------- /SearchUI/public/images/QnAMaker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /SearchUI/public/images/cognitive-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/SearchUI/public/images/cognitive-search.png -------------------------------------------------------------------------------- /SearchUI/public/images/microsoft-small.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SearchUI/public/images/microsoft.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/SearchUI/public/images/microsoft.ico -------------------------------------------------------------------------------- /SearchUI/public/images/microsoft.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SearchUI/public/images/msft-logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/SearchUI/public/images/msft-logo-small.png -------------------------------------------------------------------------------- /SearchUI/public/images/search-and-qna.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/SearchUI/public/images/search-and-qna.png -------------------------------------------------------------------------------- /SearchUI/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Cognitive Search - QnA 10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /SearchUI/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /SearchUI/public/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [ 3 | { 4 | "route": "/logout", 5 | "serve": "/.auth/logout" 6 | }, 7 | { 8 | "route": "/login", 9 | "serve": "/.auth/login/aad" 10 | }, 11 | { 12 | "route": "/.auth/login/github", 13 | "statusCode": "404" 14 | }, 15 | { 16 | "route": "/.auth/login/google", 17 | "statusCode": "404" 18 | }, 19 | { 20 | "route": "/.auth/login/facebook", 21 | "statusCode": "404" 22 | }, 23 | { 24 | "route": "/.auth/login/twitter", 25 | "statusCode": "404" 26 | }, 27 | { 28 | "route": "/home", 29 | "serve": "/", 30 | "statusCode": 301 31 | }, 32 | { 33 | "route": "/search" 34 | }, 35 | { 36 | "route": "/upload" 37 | }, 38 | { 39 | "route": "/api/search" 40 | }, 41 | { 42 | "route": "/api/lookup" 43 | }, 44 | { 45 | "route": "/api/suggest" 46 | }, 47 | { 48 | "route": "/api/upload" 49 | }, 50 | { 51 | "route": "/api/getKb" 52 | }, 53 | { 54 | "route": "/api/answer" 55 | }, 56 | { 57 | "route": "/details/*" 58 | }, 59 | { 60 | "route": "/*", 61 | "serve": "/", 62 | "statusCode": 200 63 | } 64 | ], 65 | "platformErrorOverrides": [ 66 | { 67 | "errorType": "Unauthenticated", 68 | "statusCode": 302, 69 | "serve": "/login" 70 | }, 71 | { 72 | "errorType": "NotFound", 73 | "serve": "/index.html", 74 | "statusCode": 200 75 | } 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /SearchUI/public/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /SearchUI/src/App/App.css: -------------------------------------------------------------------------------- 1 | /* Application styles */ 2 | .app { 3 | background-color: #fff; 4 | padding: 0; 5 | min-width: 375px; 6 | } 7 | 8 | .main--home { 9 | min-height: 40em; 10 | } 11 | .main--details { 12 | min-height: 40em; 13 | } 14 | -------------------------------------------------------------------------------- /SearchUI/src/App/App.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import React, {useState, useEffect} from 'react'; 5 | import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; 6 | import CircularProgress from '@material-ui/core/CircularProgress'; 7 | import axios from 'axios'; 8 | 9 | // Context for user authentication 10 | import { AuthContext } from '../contexts/AuthContext'; 11 | 12 | // App shell components 13 | import AppHeader from '../components/AppHeader/AppHeader'; 14 | //import AppFooter from '../components/AppFooter/AppFooter'; 15 | 16 | // React Router page components 17 | import Home from '../pages/Home/Home'; 18 | import Search from '../pages/Search/Search'; 19 | import Details from '../pages/Details/Details'; 20 | import Upload from '../pages/Upload/Upload'; 21 | 22 | // Bootstrap styles, optionally with jQuery and Popper 23 | import 'bootstrap/dist/css/bootstrap.min.css'; 24 | 25 | // Custom app styles 26 | import './App.css'; 27 | 28 | export default function App() { 29 | 30 | // React Hook: useState with a var name, set function, & default value 31 | const [functionCode, setFunctionCode] = useState(""); 32 | const [functionUrl, setFunctionUrl] = useState(""); 33 | const [user, setUser] = useState({}); 34 | const [knowledgeBaseID, setKnowledgeBaseID] = useState(""); 35 | 36 | // Fetch authentication API & set user state 37 | // async function fetchAuth() { 38 | // const response = await fetch("/.auth/me"); 39 | // if (response) { 40 | // const contentType = response.headers.get("content-type"); 41 | // if (contentType && contentType.indexOf("application/json") !== -1) { 42 | // response.json() 43 | // .then(response => setUser(response)) 44 | // .catch(error => console.error('Error:', error)); 45 | // } 46 | // } 47 | // } 48 | 49 | async function fetchCredentials() { 50 | const config_url = "/config"; 51 | 52 | // if NODE_ENV is development, pull variables from local .env file 53 | // otherwise, pull variables from config endpoint 54 | if (process.env.NODE_ENV === 'development') { 55 | setFunctionCode(process.env.REACT_APP_FUNCTION_CODE); 56 | setFunctionUrl(process.env.REACT_APP_FUNCTION_URL); 57 | 58 | const headers = { 59 | "x-functions-key": process.env.REACT_APP_FUNCTION_CODE 60 | }; 61 | 62 | const url = process.env.REACT_APP_FUNCTION_URL + '/api/getKb'; 63 | axios.get(url, {headers: headers}) 64 | .then(kbResponse => { 65 | setKnowledgeBaseID(kbResponse.data.qnAMakerKnowledgeBaseID) 66 | }) 67 | .catch(error => { 68 | console.log(error); 69 | }); 70 | 71 | } else { 72 | axios.get(config_url) 73 | .then(response => { 74 | setFunctionCode(response.data.code); 75 | setFunctionUrl(response.data.url); 76 | 77 | const headers = { 78 | "x-functions-key": response.data.code 79 | }; 80 | 81 | const url = response.data.url + '/api/getKb'; 82 | axios.get(url, {headers: headers}) 83 | .then(kbResponse => { 84 | setKnowledgeBaseID(kbResponse.data.qnAMakerKnowledgeBaseID) 85 | }) 86 | .catch(error => { 87 | console.log(error); 88 | }); 89 | 90 | }) 91 | .catch(error => { 92 | console.log(error); 93 | }); 94 | } 95 | } 96 | 97 | 98 | // React Hook: useEffect when component changes 99 | // Empty array ensure this only runs once on mount 100 | useEffect(() => { 101 | //fetchAuth(); 102 | fetchCredentials(); 103 | }, []); 104 | 105 | if (functionUrl !== "") { 106 | return ( 107 | 108 |
109 | 110 | 111 | 112 | } /> 113 | } /> 114 | } /> 115 |
} /> 116 | 117 | 118 | {/* */} 119 |
120 |
121 | ); 122 | } else { 123 | return ( 124 | 125 | ); 126 | } 127 | 128 | 129 | } 130 | -------------------------------------------------------------------------------- /SearchUI/src/axios.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import axios from 'axios'; 5 | 6 | const instance = axios.create({ 7 | baseURL: 'https://jsonplaceholder.typicode.com' 8 | }); 9 | 10 | instance.defaults.headers.common['Authorization'] = 'AUTH TOKEN FROM INSTANCE'; 11 | 12 | // instance.interceptors.request... 13 | 14 | export default instance; -------------------------------------------------------------------------------- /SearchUI/src/components/AppFooter/AppFooter.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | margin-top: 1em; 3 | padding: 1em; 4 | font-size: 0.85em; 5 | color: #666666; 6 | text-align: center; 7 | } 8 | -------------------------------------------------------------------------------- /SearchUI/src/components/AppFooter/AppFooter.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import React from 'react'; 5 | 6 | import './AppFooter.css'; 7 | 8 | export default function AppFooter() { 9 | return ( 10 |
11 |
12 | © 2020 Microsoft 13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /SearchUI/src/components/AppHeader/AppHeader.css: -------------------------------------------------------------------------------- 1 | .logo { 2 | margin-left: 2em; 3 | height: 60px; 4 | } 5 | 6 | .header { 7 | background-color: #0078d7; 8 | color: #eee; 9 | } 10 | .nav-link { color: #fff; } 11 | .nav-link:hover { color: #eee; } 12 | 13 | .auth-link { color: #fff; } 14 | .auth-link:hover { color: #eee; } 15 | -------------------------------------------------------------------------------- /SearchUI/src/components/AppHeader/AppHeader.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import React from 'react'; 5 | // import AppHeaderAuth from '../AppHeaderAuth/AppHeaderAuth'; 6 | 7 | import './AppHeader.css'; 8 | 9 | const iconStyle = { 10 | width: "1.5em", 11 | height: "auto" 12 | } 13 | 14 | export default function AppHeader(props) { 15 | const kbUrl = `https://www.qnamaker.ai/Edit/KnowledgeBase?kbId=${props.kbId}`; 16 | 17 | return ( 18 |
19 | 43 | 44 |
45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /SearchUI/src/components/AppHeaderAuth/AppHeaderAuth.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import React from 'react'; 5 | 6 | // React Context for Auth 7 | import { useAuth } from '../../contexts/AuthContext'; 8 | 9 | export default function AppHeaderAuth() { 10 | // React Context: User Authentication 11 | const user = useAuth(); 12 | 13 | // Dynamically update auth div based on user context 14 | const authElement = document.querySelector('.auth'); 15 | if (authElement) { 16 | // Default sign in 17 | let html = 'Sign In'; 18 | 19 | // User profile and sign out 20 | let clientPrincipal = (user && user.clientPrincipal) || null, 21 | userDetails = (clientPrincipal && clientPrincipal.userDetails) || null; 22 | 23 | if (userDetails) { 24 | html = `${userDetails} | Sign Out`; 25 | } 26 | 27 | authElement.innerHTML = html; 28 | } 29 | 30 | return ( 31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /SearchUI/src/components/DocumentViewer/DocumentViewer.css: -------------------------------------------------------------------------------- 1 | .file-container { 2 | max-width: 100%; 3 | height: 100%; 4 | width: 100%; 5 | min-height: 100%; 6 | } 7 | 8 | .image-style { 9 | max-width: 100%; 10 | } -------------------------------------------------------------------------------- /SearchUI/src/components/DocumentViewer/DocumentViewer.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import React from 'react' 5 | 6 | import './DocumentViewer.css'; 7 | 8 | export default function DocumentViewer(props) { 9 | 10 | const path = props.document.metadata_storage_path + "?" + props.sasToken; 11 | const content = props.document.content; 12 | 13 | 14 | var fileContainerHTML; 15 | if (path != null) { 16 | var pathLower = path.toLowerCase(); 17 | 18 | if (pathLower.includes(".pdf")) { 19 | fileContainerHTML = 20 | 21 | 24 | ; 25 | } 26 | else if (pathLower.includes(".txt") || pathLower.includes(".json")) { 27 | var txtHtml = content.trim(); 28 | fileContainerHTML =
 {txtHtml} 
; 29 | } 30 | else if (pathLower.includes(".las")) { 31 | fileContainerHTML = 32 | ; 33 | } 34 | else if (pathLower.includes(".jpg") || pathLower.includes(".jpeg") || pathLower.includes(".gif") || pathLower.includes(".png")) { 35 | fileContainerHTML = 36 |
37 | the search result 38 |
; 39 | } 40 | else if (pathLower.includes(".xml")) { 41 | fileContainerHTML = 42 | ; 45 | } 46 | else if (pathLower.includes(".htm")) { 47 | fileContainerHTML = 48 | ; 49 | } 50 | else if (pathLower.includes(".mp3")) { 51 | fileContainerHTML = 52 | ; 56 | } 57 | else if (pathLower.includes(".mp4")) { 58 | fileContainerHTML = 59 | ; 63 | } 64 | else if (pathLower.includes(".doc") || pathLower.includes(".ppt") || pathLower.includes(".xls")) { 65 | var src = "https://view.officeapps.live.com/op/view.aspx?src=" + encodeURIComponent(path); 66 | 67 | fileContainerHTML = 68 | ; 69 | } 70 | else { 71 | fileContainerHTML = 72 |
This file cannot be previewed. Download it here to view: Download
; 73 | } 74 | } 75 | else { 76 | fileContainerHTML = 77 |
This file cannot be previewed or downloaded.
; 78 | } 79 | 80 | return fileContainerHTML; 81 | } -------------------------------------------------------------------------------- /SearchUI/src/components/Facets/CheckboxFacet/CheckboxFacet.css: -------------------------------------------------------------------------------- 1 | .facet-checkbox { 2 | list-style-type: none; 3 | padding: 0; 4 | margin: 0; 5 | } 6 | 7 | .card-body { 8 | padding-left: 0px; 9 | } 10 | 11 | .facet-header:hover { 12 | text-decoration: underline; 13 | cursor: pointer; 14 | } 15 | -------------------------------------------------------------------------------- /SearchUI/src/components/Facets/CheckboxFacet/CheckboxFacet.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import React, {useState} from 'react'; 5 | import { Collapse, Checkbox, List, ListItem, ListItemText } from '@material-ui/core'; 6 | import { ExpandLess, ExpandMore } from '@material-ui/icons'; 7 | import styled from 'styled-components'; 8 | 9 | import './CheckboxFacet.css'; 10 | 11 | export default function CheckboxFacet(props) { 12 | 13 | let [isExpanded, setIsExpanded] = useState(false); 14 | 15 | const checkboxes = props.values.map(facetValue => { 16 | 17 | let isSelected = props.selectedFacets.some(facet => facet.value === facetValue.value); 18 | 19 | return ( 20 | 21 | props.removeFilter({field: props.name, value: facetValue.value}) : 28 | () => props.addFilter(props.name, facetValue.value) 29 | } 30 | /> 31 | 32 | 33 | ); 34 | }); 35 | 36 | 37 | return ( 38 |
39 | setIsExpanded(!isExpanded)}> 40 | 43 | {isExpanded ? : } 44 | 45 | 46 | 47 | {checkboxes} 48 | 49 | 50 |
51 | ); 52 | } 53 | 54 | const FacetListItem = styled(ListItem)({ 55 | paddingLeft: '36px !important', 56 | }) 57 | 58 | const FacetValueListItem= styled(ListItem)({ 59 | paddingLeft: '46px !important', 60 | }); 61 | 62 | const FacetValuesList= styled(List)({ 63 | maxHeight: 340, 64 | overflowY: 'auto !important', 65 | marginRight: '18px !important' 66 | }) -------------------------------------------------------------------------------- /SearchUI/src/components/Facets/Facets.css: -------------------------------------------------------------------------------- 1 | .facetbox { 2 | border-right: 1px solid #f0f0f0; 3 | } 4 | 5 | .box { 6 | height: 100%; 7 | } 8 | 9 | .listitem { 10 | padding-left: 1em !important; 11 | padding-right: 1em !important; 12 | } 13 | 14 | .filterlist { 15 | list-style: none; 16 | } 17 | 18 | .chip { 19 | margin: 0.25em; 20 | } -------------------------------------------------------------------------------- /SearchUI/src/components/Facets/Facets.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import React from 'react'; 5 | import { List, Chip } from '@material-ui/core'; 6 | import CheckboxFacet from './CheckboxFacet/CheckboxFacet'; 7 | import styled from 'styled-components'; 8 | import "./Facets.css"; 9 | 10 | export default function Facets(props) { 11 | 12 | function mapFacetName(facetName) { 13 | const capitalizeFirstLetter = (string) => 14 | string[0] ? `${string[0].toUpperCase()}${string.substring(1)}` : ''; 15 | facetName = facetName.trim(); 16 | facetName = capitalizeFirstLetter(facetName); 17 | 18 | facetName = facetName.replace('_', ' '); 19 | return facetName; 20 | } 21 | 22 | function addFilter(name, value) { 23 | const newFilters = props.filters.concat({ field: name, value: value }); 24 | props.setFilters(newFilters); 25 | } 26 | 27 | function removeFilter(filter) { 28 | const newFilters = props.filters.filter((item) => item.value !== filter.value); 29 | props.setFilters(newFilters); 30 | } 31 | 32 | var facets; 33 | try{ 34 | facets = Object.keys(props.facets).map(key => { 35 | return f.field === key)} 43 | />; 44 | }); 45 | } catch (error) { 46 | console.log(error); 47 | } 48 | 49 | const filters = props.filters.map((filter, index) => { 50 | return ( 51 |
  • 52 | removeFilter(filter)} 55 | className="chip" 56 | /> 57 |
  • ); 58 | }); 59 | 60 | 61 | return ( 62 |
    63 |
    64 |
    65 |
      66 | {filters} 67 |
    68 |
    69 | 70 | {facets} 71 | 72 |
    73 |
    74 | ); 75 | }; 76 | 77 | const FacetList = styled(List)({ 78 | marginTop: '32px !important' 79 | }) 80 | -------------------------------------------------------------------------------- /SearchUI/src/components/Pager/Pager.css: -------------------------------------------------------------------------------- 1 | .item { 2 | margin: 1em auto; 3 | } 4 | 5 | .pager { 6 | margin: auto; 7 | max-width: fit-content; 8 | } -------------------------------------------------------------------------------- /SearchUI/src/components/Pager/Pager.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import React, {useState, useEffect} from 'react'; 5 | 6 | import './Pager.css'; 7 | 8 | export default function Pager(props) { 9 | 10 | let [selectedPage, setSelectedPage] = useState(props.currentPage); 11 | let totalPages = Math.ceil(props.resultCount / props.resultsPerPage); 12 | 13 | useEffect(_=>{ 14 | props.setCurrentPage(selectedPage); 15 | }, [selectedPage, props]); 16 | 17 | function goToNextPage() { 18 | setSelectedPage(selectedPage + 1); 19 | } 20 | 21 | function goToPreviousPage() { 22 | setSelectedPage(selectedPage - 1); 23 | } 24 | 25 | var i = 0; 26 | var page_links = []; 27 | 28 | var minPage = 1; 29 | var maxPage = totalPages; 30 | 31 | if (selectedPage - minPage > 2) { 32 | minPage = selectedPage - 2; 33 | } 34 | 35 | if (maxPage - selectedPage > 2) { 36 | maxPage = parseInt(selectedPage) + 2; 37 | } 38 | 39 | 40 | for (i = minPage; i <= maxPage; i++) { 41 | if (i === parseInt(selectedPage)) { 42 | page_links.push( 43 |
  • 44 | 45 | {i} 46 | 47 |
  • 48 | ); 49 | } else { 50 | page_links.push( 51 |
  • 52 | 53 |
  • 54 | ); 55 | } 56 | } 57 | 58 | var previousButton; 59 | if (parseInt(selectedPage) === 1) { 60 | previousButton = (
  • 61 | Previous 62 |
  • ); 63 | } else { 64 | previousButton = (
  • 65 | 66 |
  • ); 67 | } 68 | 69 | var nextButton; 70 | if (parseInt(selectedPage) === totalPages) { 71 | nextButton = (
  • 72 | Next 73 |
  • ); 74 | } else { 75 | nextButton = (
  • 76 | 77 |
  • ); 78 | } 79 | 80 | 81 | 82 | return ( 83 | 90 | ); 91 | 92 | } -------------------------------------------------------------------------------- /SearchUI/src/components/Results/Answer/Answer.css: -------------------------------------------------------------------------------- 1 | .answer { 2 | 3 | padding: 16px; 4 | text-align: left; 5 | border: 1px solid #eee; 6 | box-shadow: 0 2px 3px #ccc; 7 | margin: 10px; 8 | margin-bottom: 3px; 9 | padding-bottom: 3px; 10 | box-sizing: border-box; 11 | overflow: hidden; 12 | /* cursor: pointer; */ 13 | } 14 | /* 15 | .result:hover, 16 | .result:active { 17 | background-color: #C0DDF5; 18 | } */ 19 | 20 | .result-item { 21 | margin-bottom: 2em; 22 | text-align:left; 23 | } 24 | 25 | .title-style { 26 | vertical-align: left; 27 | } 28 | 29 | em { 30 | font-weight: bold; 31 | font-style: normal; 32 | } 33 | 34 | -------------------------------------------------------------------------------- /SearchUI/src/components/Results/Answer/Answer.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import React from 'react'; 5 | import ReactMarkdown from 'react-markdown'; 6 | 7 | import './Answer.css'; 8 | 9 | 10 | export default function Answer(props) { 11 | 12 | const bodyStyle = { 13 | padding: '0.25rem', 14 | overflowWrap: 'normal' 15 | }; 16 | 17 | const pStyle = { 18 | paddingLeft: '0.25rem', 19 | fontSize: '0.9rem' 20 | }; 21 | 22 | const uriStyle = { 23 | color: 'green', 24 | paddingLeft: '0.25rem', 25 | paddingTop: '0', 26 | paddingBottom: '0', 27 | marginTop: '0', 28 | marginBottom: '0', 29 | whiteSpace: 'nowrap', 30 | overflow: 'hidden' 31 | }; 32 | 33 | return ( 34 |
    35 |
    36 |
    {props.data.answer.questions[0]}
    37 |

    38 | {props.data.answer.answer} 39 |

    40 | 41 |
    42 |
    {props.data.document.document.metadata_storage_name}
    43 |
    44 |
    45 |

    {props.data.document.document.metadata_storage_path}

    46 |

    47 |
    48 |
    49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /SearchUI/src/components/Results/Result/Result.css: -------------------------------------------------------------------------------- 1 | .result { 2 | padding: 8px; 3 | padding-left: 8px; 4 | text-align: left; 5 | margin: 10px; 6 | margin-bottom: 3px; 7 | padding-bottom: 0px; 8 | /* cursor: pointer; */ 9 | } 10 | /* 11 | .result:hover, 12 | .result:active { 13 | background-color: #C0DDF5; 14 | } */ 15 | 16 | .result-item { 17 | margin-bottom: 2em; 18 | text-align:left; 19 | } 20 | 21 | .title-style { 22 | vertical-align: left; 23 | } 24 | 25 | em { 26 | font-weight: bold; 27 | font-style: normal; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /SearchUI/src/components/Results/Result/Result.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import React from 'react' 5 | import ReactHtmlParser from 'react-html-parser'; 6 | 7 | import './Result.css'; 8 | 9 | export default function Result(props) { 10 | 11 | const cardStyle = { 12 | maxHeight: '18rem', 13 | display: 'block' 14 | }; 15 | 16 | const bodyStyle = { 17 | padding: '0.25rem', 18 | paddingBottom: '0', 19 | marginBottom: '0' 20 | }; 21 | 22 | const pStyle = { 23 | paddingLeft: '0.25rem', 24 | fontSize: '0.9rem' 25 | }; 26 | 27 | const uriStyle = { 28 | color: 'green', 29 | paddingLeft: '0.25rem', 30 | paddingTop: '0', 31 | paddingBottom: '0', 32 | marginTop: '0', 33 | marginBottom: '0', 34 | whiteSpace: 'nowrap', 35 | overflow: 'hidden' 36 | }; 37 | 38 | return ( 39 |
    40 | 41 |
    42 |
    {props.document.metadata_storage_name}
    43 |
    44 |
    45 |

    {props.document.metadata_storage_path}

    46 |

    47 | {ReactHtmlParser(props?.highlights?.content[0] || "")} 48 |

    49 |
    50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /SearchUI/src/components/Results/Results.css: -------------------------------------------------------------------------------- 1 | /* Detail Styles */ 2 | .result-item { 3 | margin-bottom: 2em; 4 | text-align:center; 5 | } 6 | .result-item-image { 7 | min-width: 150px; 8 | max-width: 200px; 9 | max-height: 300px; 10 | } 11 | 12 | .Results { 13 | display: flex; 14 | flex-flow: row wrap; 15 | justify-content: center; 16 | width: 100%; 17 | margin: auto; 18 | margin-left: 0em; 19 | margin-right: 0em; 20 | } -------------------------------------------------------------------------------- /SearchUI/src/components/Results/Results.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import React from 'react'; 5 | import Result from './Result/Result'; 6 | import Answer from './Answer/Answer'; 7 | 8 | import "./Results.css"; 9 | 10 | export default function Results(props) { 11 | 12 | const infoStyle = { 13 | margin: '1em' 14 | } 15 | 16 | let results = props.documents.map((result, index) => { 17 | return ; 22 | }); 23 | 24 | let beginDocNumber = Math.min(props.skip + 1, props.count); 25 | let endDocNumber = Math.min(props.skip + props.top, props.count); 26 | 27 | //console.log(props.documents); 28 | var answer; 29 | if(props.answer?.answer?.answer && beginDocNumber === 1 && props.answer?.answer?.answer !== "No good match found in KB.") { 30 | answer = ; 31 | } else { 32 | answer = null; 33 | } 34 | 35 | return ( 36 |
    37 |

    Showing {beginDocNumber}-{endDocNumber} of {props.count.toLocaleString()} results

    38 |
    39 | {answer} 40 |
    41 |
    42 | {results} 43 |
    44 |
    45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /SearchUI/src/components/SearchBar/SearchBar.css: -------------------------------------------------------------------------------- 1 | div.container a.input-group-btn { 2 | font-size: 14px; 3 | } 4 | 5 | .suggestions { 6 | position: relative; 7 | display: inline-block; 8 | width: inherit; 9 | z-index:99 10 | } 11 | 12 | .input-group { 13 | flex-wrap: nowrap; 14 | } -------------------------------------------------------------------------------- /SearchUI/src/components/SearchBar/SearchBar.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import React, {useState, useEffect} from 'react'; 5 | import axios from 'axios'; 6 | import Suggestions from './Suggestions/Suggestions'; 7 | 8 | import "./SearchBar.css"; 9 | 10 | export default function SearchBar(props) { 11 | 12 | let [q, setQ] = useState(""); 13 | let [suggestions, setSuggestions] = useState([]); 14 | let [showSuggestions, setShowSuggestions] = useState(false); 15 | 16 | const onSearchHandler = () => { 17 | props.postSearchHandler(q); 18 | setShowSuggestions(false); 19 | } 20 | 21 | const onEnterButton = (event) => { 22 | if (event.keyCode === 13) { 23 | onSearchHandler(); 24 | } 25 | } 26 | 27 | const suggestionClickHandler = (s) => { 28 | document.getElementById("search-box").value = s; 29 | setShowSuggestions(false); 30 | props.postSearchHandler(s); 31 | 32 | } 33 | 34 | const onChangeHandler = () => { 35 | var searchTerm = document.getElementById("search-box").value; 36 | setShowSuggestions(true); 37 | setQ(searchTerm); 38 | 39 | // use this prop if you want to make the search more reactive 40 | if (props.searchChangeHandler) { 41 | props.searchChangeHandler(searchTerm); 42 | } 43 | } 44 | 45 | useEffect(_ =>{ 46 | const timer = setTimeout(() => { 47 | const body = { 48 | q: q, 49 | top: 5, 50 | suggester: 'sg' 51 | }; 52 | 53 | const headers = { 54 | "x-functions-key": props.code 55 | }; 56 | 57 | if (q === '') { 58 | setSuggestions([]); 59 | } else { 60 | const url = props.url + '/api/suggest'; 61 | axios.post( url, body, {headers: headers}) 62 | .then( response => { 63 | setSuggestions(response.data.suggestions); 64 | } ) 65 | .catch(error => { 66 | console.log(error); 67 | setSuggestions([]); 68 | }); 69 | } 70 | }, 300); 71 | return () => clearTimeout(timer); 72 | }, [q, props]); 73 | 74 | var suggestionDiv; 75 | if (showSuggestions) { 76 | suggestionDiv = ( suggestionClickHandler(s)}>); 77 | } else { 78 | suggestionDiv = (
    ); 79 | } 80 | 81 | return ( 82 |
    83 |
    onEnterButton(e)}> 84 |
    85 | setShowSuggestions(false)} 94 | onClick={() => setShowSuggestions(true)}> 95 | 96 | {suggestionDiv} 97 |
    98 |
    99 | 102 |
    103 |
    104 |
    105 | ); 106 | }; -------------------------------------------------------------------------------- /SearchUI/src/components/SearchBar/Suggestions/Suggestions.css: -------------------------------------------------------------------------------- 1 | .suggestion-item:hover { 2 | /*when hovering an item:*/ 3 | background-color: #e9e9e9; 4 | cursor: pointer; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /SearchUI/src/components/SearchBar/Suggestions/Suggestions.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import React from 'react'; 5 | 6 | import "./Suggestions.css"; 7 | import 'bootstrap/dist/css/bootstrap.min.css'; 8 | 9 | export default function Suggestions(props) { 10 | 11 | const suggestionClickHandler = (e) => { 12 | props.suggestionClickHandler(e.currentTarget.id); 13 | } 14 | 15 | const borders = { 16 | border: "1px solid #eee", 17 | boxShadow: "0 2px 3px #ccc", 18 | boxSizing: "border-box" 19 | } 20 | 21 | let suggestions = props.suggestions.map((s, index) => { 22 | return (
    {s.queryPlusText}
    ); 23 | }); 24 | 25 | return ( 26 |
    27 | {suggestions} 28 |
    29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /SearchUI/src/components/Transcript/Transcript.css: -------------------------------------------------------------------------------- 1 | pre { 2 | font-family: 'Segoe UI', sans-serif; 3 | font-size: 14px; 4 | white-space: pre-line; 5 | word-break: keep-all; 6 | } 7 | 8 | .scroll { 9 | overflow-y: auto; 10 | } 11 | 12 | .highlight { 13 | background-color: rgb(160, 197, 232); 14 | } 15 | -------------------------------------------------------------------------------- /SearchUI/src/components/Transcript/Transcript.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import React, {useEffect} from 'react' 5 | import ReactHtmlParser from 'react-html-parser'; 6 | 7 | import './Transcript.css'; 8 | 9 | 10 | export default function Transcript(props) { 11 | 12 | 13 | useEffect(_ =>{ 14 | console.log(props.highlight); 15 | if (!!props.highlight) { 16 | 17 | let highlightedElement = document.getElementById(props.highlight) 18 | if (!!highlightedElement) { 19 | highlightedElement.scrollIntoView({block: 'start', behavior: 'smooth'}); 20 | } 21 | } 22 | }, [props]); 23 | 24 | 25 | let full_content = ""; 26 | 27 | // If we have merged content, let's use it. 28 | if (props.document.merged_content) { 29 | if (props.document.merged_content.length > 0) { 30 | full_content = props.document.merged_content.trim(); 31 | } 32 | } 33 | else { 34 | // otherwise, let's try getting the content -- although it won't have any image data. 35 | full_content = props.document.content.trim(); 36 | } 37 | 38 | if (full_content === null || full_content === "") { 39 | // not much to display 40 | return null; 41 | } 42 | 43 | // finds all matches to the search term in the transcript and adds a highlight class to them (plus an id that can be used for scrolling) 44 | function GetReferences(searchText, content) { 45 | // find all matches in content 46 | var regex = new RegExp(searchText, 'gi'); 47 | 48 | var i = -1; 49 | var response = content.replace(regex, function (str) { 50 | i++; 51 | var shortname = str.slice(0, 20).replace(/[^a-zA-Z ]/g, " ").replace(new RegExp(" ", 'g'), "_"); 52 | return `${str}`; 53 | }) 54 | 55 | return response; 56 | } 57 | 58 | 59 | if (props.q.trim() !== "") { 60 | full_content = GetReferences(props.q, full_content); 61 | } 62 | 63 | 64 | return ( 65 |
    66 | 67 | 68 | 69 | 72 | 73 | 74 |
    70 |
    {ReactHtmlParser(full_content)}
    71 |
    75 |
    76 | ); 77 | } -------------------------------------------------------------------------------- /SearchUI/src/contexts/AuthContext.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import { createContext, useContext } from 'react'; 5 | 6 | // Create new auth context 7 | export const AuthContext = createContext(); 8 | 9 | // React hook to use auth context 10 | export function useAuth() { 11 | return useContext(AuthContext); 12 | } 13 | -------------------------------------------------------------------------------- /SearchUI/src/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | 7 | import App from './App/App'; 8 | 9 | ReactDOM.render( 10 | 11 | 12 | , 13 | document.getElementById('root') 14 | ); 15 | -------------------------------------------------------------------------------- /SearchUI/src/pages/Details/Details.css: -------------------------------------------------------------------------------- 1 | .main--details { 2 | padding-top: 1em; 3 | width: 100%; 4 | height: 100%; 5 | }; 6 | 7 | .image { 8 | width: 10em; 9 | height: auto; 10 | } 11 | 12 | .nav-tabs { 13 | margin-left: 1em; 14 | 15 | } 16 | 17 | .black { 18 | color: black; 19 | } 20 | 21 | .black:hover { 22 | color: darkgray; 23 | } 24 | 25 | .result-container { 26 | min-height: 40em; 27 | margin: 10px; 28 | border: 1px solid #ccc; 29 | max-height: 100%; 30 | } 31 | 32 | #tags-panel { 33 | margin-top: 1em; 34 | } 35 | 36 | .tag-container { 37 | display: flex; 38 | flex-flow: row wrap; 39 | justify-content: flex-start; 40 | width: 100%; 41 | margin-top: 1em; 42 | } 43 | 44 | .tag { 45 | background-color: #0078d7; 46 | border: 1px solid transparent; 47 | border-radius: 0; 48 | color: #fff; 49 | display: inline-block; 50 | font-size: 13px; 51 | line-height: 1; 52 | margin: 2px 2px 2px 0; 53 | max-width: 100%; 54 | opacity: 1; 55 | padding: .4em .5em; 56 | position: relative; 57 | text-align: center; 58 | text-decoration: none; 59 | transition: all .15s ease-in-out; 60 | white-space: nowrap; 61 | } 62 | -------------------------------------------------------------------------------- /SearchUI/src/pages/Details/Details.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import React, { useState, useEffect, useRef } from "react"; 5 | import { useParams } from 'react-router-dom'; 6 | import CircularProgress from '@material-ui/core/CircularProgress'; 7 | import Transcript from '../../components/Transcript/Transcript'; 8 | import DocumentViewer from '../../components/DocumentViewer/DocumentViewer'; 9 | import ReactHtmlParser from 'react-html-parser'; 10 | import axios from 'axios'; 11 | import "./Details.css"; 12 | 13 | export default function Details(props) { 14 | 15 | let { id } = useParams(); 16 | const [document, setDocument] = useState({}); 17 | const [sasToken, setSasToken] = useState(""); 18 | const [selectedTab, setTab] = useState(0); 19 | const [highlight, setHighlight] = useState(null); 20 | const [isLoading, setIsLoading] = useState(true); 21 | const [q, setQ] = useState(""); 22 | const searchBar = useRef(null); 23 | 24 | useEffect(() => { 25 | setIsLoading(true); 26 | 27 | const headers = { 28 | "x-functions-key": props.code 29 | }; 30 | 31 | const url = props.url + '/api/lookup?id=' + id; 32 | console.log(url); 33 | axios.get(url, {headers: headers}) 34 | .then(response => { 35 | const doc = response.data.document; 36 | const sas = response.data.sasToken; 37 | setDocument(doc); 38 | setSasToken(sas); 39 | setIsLoading(false); 40 | }) 41 | .catch(error => { 42 | console.log(error); 43 | setIsLoading(false); 44 | }); 45 | 46 | }, [id]); 47 | 48 | useEffect(() => { 49 | setHighlight(null); 50 | }, [q]); 51 | 52 | function GetTagsHTML(tags) { 53 | 54 | if (!!tags) { 55 | let tagsHtml = tags.map((tagValue, index) => { 56 | if (index < 10) { 57 | 58 | if (tagValue.length > 30) { // check tag name length 59 | // create substring of tag name length if too long 60 | tagValue = tagValue.slice(0, 30); 61 | } 62 | 63 | return ; 64 | } else { 65 | return null; 66 | } 67 | }); 68 | 69 | return tagsHtml; 70 | } 71 | 72 | return null; 73 | } 74 | 75 | let tags = GetTagsHTML(document.keyPhrases); 76 | 77 | function GetSnippets(q, content) { 78 | if (!!content && q.trim() !== "") { 79 | var regex = new RegExp(q, 'gi'); 80 | 81 | let matches = content.match(regex); 82 | 83 | return matches.map((value, index) => { 84 | var startIdx; 85 | var maxLengthOfSnippet = 400; 86 | var ln = maxLengthOfSnippet; 87 | 88 | if (value.length > 150) { 89 | startIdx = content.indexOf(value); 90 | ln = value.length; 91 | } 92 | else { 93 | if (content.indexOf(value) < (maxLengthOfSnippet / 2)) { 94 | startIdx = 0; 95 | } 96 | else { 97 | startIdx = content.indexOf(value) - (maxLengthOfSnippet / 2); 98 | } 99 | 100 | ln = maxLengthOfSnippet + value.length; 101 | } 102 | 103 | var reference = content.slice(startIdx, startIdx + ln); 104 | content = content.replace(value, ""); 105 | 106 | reference = reference.replace(value, function (str) { 107 | return (`${str}`); 108 | }); 109 | 110 | var shortName = value.slice(0, 20).replace(/[^a-zA-Z ]/g, " ").replace(new RegExp(" ", 'g'), "_"); 111 | 112 | return
  • ClickSnippet(`${index}_${shortName}`)}>{ReactHtmlParser(reference)}
  • ; 113 | 114 | }); 115 | } 116 | } 117 | 118 | let snippets = GetSnippets(q, document.content); 119 | 120 | function ClickSnippet(name) { 121 | // navigating to the transcript 122 | setTab(1); 123 | setHighlight(name); 124 | } 125 | 126 | var body; 127 | let tab_0_style = "nav-link black"; 128 | let tab_1_style = "nav-link black"; 129 | let tab_2_style = "nav-link black"; 130 | if (isLoading) { 131 | body = (); 132 | } else { 133 | if (selectedTab === 0) { 134 | body = (); 135 | tab_0_style = "nav-link active black"; 136 | } 137 | else if (selectedTab === 1) { 138 | body = (); 139 | tab_1_style = "nav-link active black"; 140 | } 141 | else if (selectedTab === 2) { 142 | body =
    143 |
    144 |           {JSON.stringify(document, null, 2)}
    145 |         
    146 |
    ; 147 | tab_2_style = "nav-link active black"; 148 | } 149 | 150 | } 151 | 152 | 153 | 154 | return ( 155 |
    156 |
    157 |
    158 |
      159 |
    • 160 | 161 |
    • 162 |
    • 163 | 164 |
    • 165 |
    • 166 | 167 |
    • 168 |
    169 |
    170 |
    171 |
    172 | {body} 173 |
    174 | 175 |
    176 |
    177 | 195 |
    196 | {tags} 197 |
    198 |
    199 |
    200 | {snippets} 201 |
    202 |
    203 |
    204 |
    205 |
    206 |
    207 | ); 208 | } 209 | 210 | -------------------------------------------------------------------------------- /SearchUI/src/pages/Home/Home.css: -------------------------------------------------------------------------------- 1 | .home-search { 2 | margin: 5em auto; 3 | max-width: 45%; 4 | display: block; 5 | } 6 | 7 | .logo { 8 | height: 12em; 9 | width: auto; 10 | display:block; 11 | margin: auto auto 0; 12 | } 13 | 14 | .poweredby { 15 | text-align: center; 16 | } -------------------------------------------------------------------------------- /SearchUI/src/pages/Home/Home.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import React from "react"; 5 | import { useHistory } from "react-router-dom"; 6 | 7 | import SearchBar from '../../components/SearchBar/SearchBar'; 8 | 9 | import "./Home.css"; 10 | import "../../pages/Search/Search.css"; 11 | 12 | export default function Home(props) { 13 | const history = useHistory(); 14 | const navigateToSearchPage = (q) => { 15 | if (!q || q === '') { 16 | q = '*' 17 | } 18 | history.push('/search?q=' + q); 19 | } 20 | 21 | return ( 22 |
    23 |
    24 | Cognitive Search and QnA Maker 25 |

    Powered by Azure Cognitive Search and QnA Maker

    26 | 27 |
    28 |
    29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /SearchUI/src/pages/Search/Search.css: -------------------------------------------------------------------------------- 1 | .sui-layout-header { 2 | background-color: #0078d7; 3 | color: #eee; 4 | } 5 | .sui-search-box__submit { 6 | background: linear-gradient(rgb(60, 226, 102), rgb(34, 151, 57)); 7 | letter-spacing: 0.1em; 8 | } 9 | .sui-search-box__submit:hover { 10 | background: linear-gradient(rgb(34, 151, 57), rgb(60, 226, 102)); 11 | } 12 | 13 | .pager-style { 14 | margin-left: auto; 15 | margin-right: auto; 16 | max-width: fit-content; 17 | } 18 | 19 | .search-bar { 20 | margin: 1em; 21 | margin-bottom: 1em; 22 | margin-top: 2em; 23 | } 24 | 25 | .facets { 26 | margin: 1em; 27 | margin-bottom: 1em; 28 | margin-top: 2em; 29 | } -------------------------------------------------------------------------------- /SearchUI/src/pages/Search/Search.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import React, { useEffect, useState } from 'react'; 5 | import axios from 'axios'; 6 | import CircularProgress from '@material-ui/core/CircularProgress'; 7 | import { useLocation, useHistory } from "react-router-dom"; 8 | 9 | import Results from '../../components/Results/Results'; 10 | import Pager from '../../components/Pager/Pager'; 11 | import Facets from '../../components/Facets/Facets'; 12 | import SearchBar from '../../components/SearchBar/SearchBar'; 13 | 14 | import "./Search.css"; 15 | 16 | export default function Search(props) { 17 | 18 | let location = useLocation(); 19 | let history = useHistory(); 20 | 21 | const [answer, setAnswer] = useState({}); 22 | const [results, setResults] = useState([]); 23 | const [resultCount, setResultCount] = useState(0); 24 | const [currentPage, setCurrentPage] = useState(1); 25 | const [q, setQ] = useState(new URLSearchParams(location.search).get('q') ?? "*"); 26 | const [top] = useState(new URLSearchParams(location.search).get('top') ?? 8); 27 | const [skip, setSkip] = useState(new URLSearchParams(location.search).get('skip') ?? 0); 28 | const [filters, setFilters] = useState([]); 29 | const [facets, setFacets] = useState({}); 30 | const [isLoading, setIsLoading] = useState(true); 31 | 32 | let resultsPerPage = top; 33 | 34 | useEffect(() => { 35 | setIsLoading(true); 36 | setSkip((currentPage - 1) * top); 37 | const body = { 38 | q: q, 39 | top: top, 40 | skip: skip, 41 | filters: filters, 42 | // only return answer on first page 43 | getAnswer: currentPage === 1 ? true : false 44 | }; 45 | 46 | const headers = { 47 | "x-functions-key": props.code 48 | }; 49 | 50 | const url = props.url + '/api/search'; 51 | axios.post(url, body, {headers: headers}) 52 | .then(response => { 53 | setResults(response.data.results); 54 | setFacets(response.data.facets); 55 | setResultCount(response.data.count); 56 | 57 | if (currentPage === 1) { 58 | setAnswer(response.data.answers); 59 | } 60 | setIsLoading(false); 61 | }) 62 | .catch(error => { 63 | console.log(error); 64 | setIsLoading(false); 65 | }); 66 | 67 | // eslint-disable-next-line react-hooks/exhaustive-deps 68 | }, [top, skip, filters, currentPage]); 69 | 70 | // pushing the new search term to history when q is updated 71 | // allows the back button to work as expected when coming back from the details page 72 | useEffect(() => { 73 | history.push('/search?q=' + q); 74 | setCurrentPage(1); 75 | setFilters([]); 76 | 77 | // eslint-disable-next-line react-hooks/exhaustive-deps 78 | }, [q]); 79 | 80 | 81 | let postSearchHandler = (searchTerm) => { 82 | //console.log(searchTerm); 83 | setQ(searchTerm); 84 | } 85 | 86 | 87 | 88 | var body; 89 | if (isLoading) { 90 | body = ( 91 |
    92 | 93 |
    ); 94 | } else { 95 | body = ( 96 |
    97 | 98 | 99 |
    100 | ) 101 | } 102 | 103 | return ( 104 |
    105 | 106 |
    107 |
    108 |
    109 | 110 |
    111 | 112 |
    113 | {body} 114 |
    115 |
    116 | ); 117 | } 118 | 119 | 120 | -------------------------------------------------------------------------------- /SearchUI/src/pages/Upload/Upload.css: -------------------------------------------------------------------------------- 1 | .main--upload { 2 | padding:3em; 3 | width: fit-content; 4 | } 5 | 6 | .upload-text { 7 | font-size: 3em; 8 | } 9 | 10 | input[type="file"] { 11 | display: none; 12 | } 13 | .custom-file-upload { 14 | border: 1px solid #ccc; 15 | display: inline-block; 16 | padding: 6px 12px; 17 | cursor: pointer; 18 | } 19 | 20 | .file-select { 21 | display: inline-block; 22 | vertical-align: middle; 23 | } 24 | 25 | .file-name { 26 | display: inline-block; 27 | padding-left: 1em; 28 | } -------------------------------------------------------------------------------- /SearchUI/src/pages/Upload/Upload.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import React, { useState } from "react"; 5 | import axios from 'axios'; 6 | import CircularProgress from '@material-ui/core/CircularProgress'; 7 | 8 | import "./Upload.css"; 9 | 10 | export default function Upload(props) { 11 | 12 | const [file, setFile] = useState(""); 13 | const [isLoading, setIsLoading] = useState(false); 14 | const [isSuccess, setIsSuccess] = useState(false); 15 | const [isError, setIsError] = useState(false); 16 | 17 | function onFileChange(event) { 18 | let selectedFile = event.target.files[0]; 19 | console.log(selectedFile); 20 | setFile(selectedFile); 21 | 22 | setIsError(false); 23 | setIsSuccess(false); 24 | } 25 | 26 | async function onUploadClick() { 27 | setIsLoading(true); 28 | setIsError(false); 29 | setIsSuccess(false); 30 | 31 | var reader = new FileReader(); 32 | 33 | reader.onload = function () { 34 | let base64File = reader.result.replace(/^data:.+;base64,/, ''); 35 | 36 | 37 | let body = { 38 | name: file.name, 39 | file: base64File, 40 | fileType: file.type 41 | } 42 | 43 | const headers = { 44 | "x-functions-key": props.code 45 | }; 46 | 47 | const url = props.url + '/api/upload'; 48 | axios.post(url, body, {headers: headers}) 49 | .then(response => { 50 | setIsLoading(false); 51 | setIsSuccess(true); 52 | }) 53 | .catch(error => { 54 | console.log(error); 55 | setIsLoading(false); 56 | setIsError(true); 57 | }); 58 | 59 | console.log(body); 60 | }; 61 | reader.onerror = function (error) { 62 | console.log('Error: ', error); 63 | }; 64 | 65 | reader.readAsDataURL(file); 66 | 67 | } 68 | 69 | var loading; 70 | if (isLoading) { 71 | loading = (); 72 | } 73 | 74 | var successMessage; 75 | if (isSuccess) { 76 | successMessage = (); 79 | } 80 | 81 | var errorMessage; 82 | if (isError) { 83 | errorMessage = (); 86 | } 87 | 88 | var button; 89 | if (file !== "") { 90 | button = (); 91 | } else { 92 | button = (); 93 | } 94 | 95 | 96 | return ( 97 |
    98 |
    99 |

    Upload a document

    100 |

    Use the buttons below to select a file to upload into the solution.

    101 |
    102 | 103 |
    104 | 105 |
    106 | 109 | onFileChange(e)} /> 110 |

    {file.name}

    111 | 112 |
    113 |
    114 | {button} 115 | {loading} 116 |
    117 |
    118 | {successMessage} 119 | {errorMessage} 120 |
    121 | ); 122 | }; 123 | -------------------------------------------------------------------------------- /SearchUI/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 5 | // allows you to do things like: 6 | // expect(element).toHaveTextContent(/react/i) 7 | // learn more: https://github.com/testing-library/jest-dom 8 | import '@testing-library/jest-dom/extend-expect'; 9 | -------------------------------------------------------------------------------- /SearchUI/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: Arial, Helvetica, sans-serif; 3 | } 4 | 5 | html, 6 | body { 7 | margin: 0; 8 | border: 0; 9 | padding: 0; 10 | background-color: #fff; 11 | } 12 | 13 | main { 14 | margin: auto; 15 | width: 50%; 16 | padding: 20px; 17 | } 18 | 19 | main { 20 | text-align: center; 21 | } 22 | 23 | h1 { 24 | font-size: 3.5em; 25 | } 26 | -------------------------------------------------------------------------------- /azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "resourcePrefix": { 6 | "type": "string", 7 | "defaultValue": "qna", 8 | "metadata": { 9 | "description": "Prefix for all resources created by this template" 10 | } 11 | }, 12 | "searchServiceSku": { 13 | "type": "string", 14 | "defaultValue": "basic", 15 | "allowedValues": [ 16 | "free", 17 | "basic", 18 | "standard", 19 | "standard2", 20 | "standard3", 21 | "storage_optimized_l1", 22 | "storage_optimized_l2" 23 | ], 24 | "metadata": { 25 | "description": "The SKU of the search service you want to create" 26 | } 27 | }, 28 | "storageAccountType": { 29 | "type": "string", 30 | "defaultValue": "Standard_LRS", 31 | "allowedValues": [ 32 | "Standard_LRS", 33 | "Standard_GRS", 34 | "Standard_RAGRS", 35 | "Standard_ZRS", 36 | "Premium_LRS", 37 | "Premium_ZRS", 38 | "Standard_GZRS", 39 | "Standard_RAGZRS" 40 | ], 41 | "metadata": { 42 | "description": "Storage Account type" 43 | } 44 | }, 45 | "serverFarmsSku": { 46 | "defaultValue": "S1", 47 | "allowedValues": [ 48 | "D1", 49 | "B1", 50 | "B2", 51 | "B3", 52 | "S1", 53 | "S2", 54 | "S3", 55 | "P1", 56 | "P2", 57 | "P3", 58 | "P4" 59 | ], 60 | "type": "String", 61 | "metadata": { 62 | "description": "The SKU of the app service hosting plan" 63 | } 64 | } 65 | }, 66 | "variables": { 67 | "repoURL": "https://github.com/Azure-Samples/search-qna-maker-accelerator", 68 | "repoDirectory": "Azure-Samples/search-qna-maker-accelerator/", 69 | "branch": "main", 70 | "reactProject": "SearchUI", 71 | "functionProject": "CustomSkillForDataIngestion\\QnAIntegrationCustomSkill\\QnAIntegrationCustomSkill.csproj", 72 | "qnaServiceName": "[concat(parameters('resourcePrefix'), '-qna-', uniqueString(resourceGroup().id, deployment().name))]", 73 | "searchServiceName": "[concat(parameters('resourcePrefix'), '-search-service-', uniqueString(resourceGroup().id, deployment().name))]", 74 | "hostingPlanName": "[concat(parameters('resourcePrefix'), '-plan-', uniqueString(resourceGroup().id, deployment().name))]", 75 | "qnaAppServiceName": "[concat(parameters('resourcePrefix'), '-site-', uniqueString(resourceGroup().id, deployment().name))]", 76 | "storageAccountName": "[concat(parameters('resourcePrefix'), 'storage', uniqueString(resourceGroup().id, deployment().name))]", 77 | "functionAppName": "[concat(parameters('resourcePrefix'), '-function-app-', uniqueString(resourceGroup().id, deployment().name))]", 78 | "cognitiveServicesAllInOneName": "[concat(parameters('resourcePrefix'), '-cogsvc-allinone-', uniqueString(resourceGroup().id, deployment().name))]", 79 | "appInsightsName": "[concat(parameters('resourcePrefix'), '-insights-', uniqueString(resourceGroup().id, deployment().name))]", 80 | "siteName": "[concat(parameters('resourcePrefix'), '-ui-', uniqueString(resourceGroup().id, deployment().name))]" 81 | }, 82 | "resources": [ 83 | { 84 | "type": "Microsoft.CognitiveServices/accounts", 85 | "apiVersion": "2017-04-18", 86 | "name": "[variables('qnaServiceName')]", 87 | "location": "westus", 88 | "dependsOn": [ 89 | "[resourceId('Microsoft.Web/Sites', variables('qnaAppServiceName'))]", 90 | "[resourceId('Microsoft.Search/searchServices', variables('searchServiceName'))]" 91 | ], 92 | "sku": { 93 | "name": "S0" 94 | }, 95 | "kind": "QnAMaker", 96 | "properties": { 97 | "apiProperties": { 98 | "qnaRuntimeEndpoint": "[concat('https://',reference(resourceId('Microsoft.Web/sites', variables('qnaAppServiceName'))).hostNames[0])]" 99 | }, 100 | "customSubDomainName": "[variables('qnaServiceName')]" 101 | } 102 | }, 103 | { 104 | "type": "Microsoft.Search/searchServices", 105 | "apiVersion": "2020-08-01", 106 | "name": "[variables('searchServiceName')]", 107 | "location": "[resourceGroup().location]", 108 | "tags": {}, 109 | "sku": { 110 | "name": "[parameters('searchServiceSku')]" 111 | }, 112 | "properties": { 113 | "replicaCount": 1, 114 | "partitionCount": 1 115 | } 116 | }, 117 | { 118 | "type": "Microsoft.Web/serverfarms", 119 | "apiVersion": "2016-09-01", 120 | "name": "[variables('hostingPlanName')]", 121 | "location": "[resourceGroup().location]", 122 | "sku": { 123 | "Name": "[parameters('serverFarmsSku')]" 124 | }, 125 | "properties": { 126 | "name": "[variables('hostingPlanName')]", 127 | "workerSizeId": "0", 128 | "reserved": false, 129 | "numberOfWorkers": "1", 130 | "hostingEnvironment": "" 131 | } 132 | }, 133 | { 134 | "type": "microsoft.insights/components", 135 | "apiVersion": "2020-02-02-preview", 136 | "name": "[variables('appInsightsName')]", 137 | "location": "[resourceGroup().location]", 138 | "tags": { 139 | "[concat('hidden-link:', resourceId('Microsoft.Web/sites', variables('appInsightsName')))]": "Resource" 140 | }, 141 | "properties": { 142 | "ApplicationId": "[variables('appInsightsName')]", 143 | "Request_Source": "IbizaWebAppExtensionCreate" 144 | } 145 | }, 146 | { 147 | "type": "Microsoft.Web/sites", 148 | "apiVersion": "2019-08-01", 149 | "name": "[variables('qnaAppServiceName')]", 150 | "location": "[resourceGroup().location]", 151 | "dependsOn": [ 152 | "[concat('Microsoft.Web/serverfarms/', variables('hostingPlanName'))]", 153 | "[resourceId('Microsoft.Search/searchServices/', variables('searchServiceName'))]", 154 | "[resourceId('microsoft.insights/components',variables('appInsightsName'))]" 155 | ], 156 | "tags": { 157 | "[concat('hidden-related:', '/subscriptions/', subscription().SubscriptionId,'/resourcegroups/', resourceGroup().name, '/providers/Microsoft.Web/serverfarms/', variables('hostingPlanName'))]": "empty" 158 | }, 159 | "properties": { 160 | "enabled": true, 161 | "siteConfig": { 162 | "cors": { 163 | "allowedOrigins": [ 164 | "*" 165 | ] 166 | }, 167 | "alwaysOn": true, 168 | "appSettings": [ 169 | { 170 | "name": "AzureSearchName", 171 | "value": "[variables('searchServiceName')]" 172 | }, 173 | { 174 | "name": "AzureSearchAdminKey", 175 | "value": "[listAdminKeys(resourceId('Microsoft.Search/searchServices', variables('searchServiceName')), '2020-08-01').primaryKey]" 176 | }, 177 | { 178 | "name": "PrimaryEndpointKey", 179 | "value": "[concat(variables('qnaAppServiceName'), '-PrimaryEndpointKey')]" 180 | }, 181 | { 182 | "name": "SecondaryEndpointKey", 183 | "value": "[concat(variables('qnaAppServiceName'), '-SecondaryEndpointKey')]" 184 | }, 185 | { 186 | "name": "DefaultAnswer", 187 | "value": "No good match found in KB." 188 | }, 189 | { 190 | "name": "QNAMAKER_EXTENSION_VERSION", 191 | "value": "latest" 192 | }, 193 | { 194 | "name": "UserAppInsightsKey", 195 | "value": "[reference(resourceId('microsoft.insights/components', variables('appInsightsName')), '2020-02-02-preview').InstrumentationKey]" 196 | } 197 | ] 198 | }, 199 | "name": "[variables('qnaAppServiceName')]", 200 | "serverFarmId": "[concat('/subscriptions/', subscription().SubscriptionId,'/resourcegroups/', resourceGroup().name, '/providers/Microsoft.Web/serverfarms/', variables('hostingPlanName'))]", 201 | "hostingEnvironment": "" 202 | } 203 | }, 204 | { 205 | "type": "Microsoft.Storage/storageAccounts", 206 | "name": "[variables('storageAccountName')]", 207 | "apiVersion": "2019-06-01", 208 | "location": "[resourceGroup().location]", 209 | "kind": "Storage", 210 | "sku": { 211 | "name": "[parameters('storageAccountType')]" 212 | } 213 | }, 214 | { 215 | "apiVersion": "2019-08-01", 216 | "name": "[variables('functionAppName')]", 217 | "type": "Microsoft.Web/sites", 218 | "kind": "functionapp", 219 | "dependsOn": [ 220 | "[resourceId('Microsoft.Web/serverfarms/', variables('hostingPlanName'))]", 221 | "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", 222 | "[resourceId('Microsoft.Search/searchServices', variables('searchServiceName'))]", 223 | "[resourceId('Microsoft.CognitiveServices/accounts', variables('qnaServiceName'))]", 224 | "[resourceId('Microsoft.CognitiveServices/accounts', variables('cognitiveServicesAllInOneName'))]", 225 | "[resourceId('microsoft.insights/components',variables('appInsightsName'))]" 226 | ], 227 | "location": "[resourceGroup().location]", 228 | "properties": { 229 | "name": "[variables('functionAppName')]", 230 | "kind": "functionapp", 231 | "httpsOnly": true, 232 | "serverFarmId": "[concat('/subscriptions/', subscription().SubscriptionId,'/resourcegroups/', resourceGroup().name, '/providers/Microsoft.Web/serverfarms/', variables('hostingPlanName'))]", 233 | "siteConfig": { 234 | "enabled": true, 235 | "alwaysOn": true, 236 | "cors": { 237 | "allowedOrigins": [ 238 | "*" 239 | ] 240 | }, 241 | "appSettings": [ 242 | { 243 | "name": "AzureWebJobsStorage", 244 | "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listkeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value, ';')]" 245 | }, 246 | { 247 | "name": "AzureWebJobsDashboard", 248 | "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listkeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value, ';')]" 249 | }, 250 | { 251 | "name": "FUNCTIONS_EXTENSION_VERSION", 252 | "value": "~3" 253 | }, 254 | { 255 | "name": "FUNCTIONS_EXTENSION_RUNTIME", 256 | "value": "dotnet" 257 | }, 258 | { 259 | "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", 260 | "value": true 261 | }, 262 | { 263 | "name": "PROJECT", 264 | "value": "[variables('functionProject')]" 265 | }, 266 | { 267 | "name": "SearchServiceName", 268 | "value": "[variables('searchServiceName')]" 269 | }, 270 | { 271 | "name": "SearchServiceApiKey", 272 | "value": "[listAdminKeys(resourceId('Microsoft.Search/searchServices', variables('searchServiceName')), '2020-08-01').primaryKey]" 273 | }, 274 | { 275 | "name": "QnAServiceName", 276 | "value": "[variables('qnaServiceName')]" 277 | }, 278 | { 279 | "name": "QnAAuthoringKey", 280 | "value": "[listkeys(resourceId('Microsoft.CognitiveServices/accounts', variables('qnaServiceName')), '2017-04-18').key1]" 281 | }, 282 | { 283 | "name": "APPINSIGHTS_INSTRUMENTATIONKEY", 284 | "value": "[reference(resourceId('microsoft.insights/components', variables('appInsightsName')), '2020-02-02-preview').InstrumentationKey]" 285 | }, 286 | { 287 | "name": "CogServicesKey", 288 | "value": "[listkeys(resourceId('Microsoft.CognitiveServices/accounts', variables('cognitiveServicesAllInOneName')), '2017-04-18').key1]" 289 | }, 290 | { 291 | "name": "StorageAccountName", 292 | "value": "[variables('storageAccountName')]" 293 | }, 294 | { 295 | "name": "StorageAccountKey", 296 | "value": "[listkeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value]" 297 | }, 298 | { 299 | "name": "QnAMakerEndpoint", 300 | "value": "[concat('https://', variables('qnaAppServiceName'), '.azurewebsites.net')]" 301 | } 302 | ] 303 | } 304 | }, 305 | "resources": [ 306 | { 307 | "type": "sourcecontrols", 308 | "apiVersion": "2019-08-01", 309 | "name": "web", 310 | "dependsOn": [ 311 | "[resourceId('Microsoft.Web/Sites',variables('functionAppName'))]" 312 | ], 313 | "properties": { 314 | "RepoUrl": "[variables('repoURL')]", 315 | "branch": "[variables('branch')]", 316 | "project": "[variables('functionProject')]", 317 | "IsManualIntegration": true 318 | } 319 | } 320 | ] 321 | }, 322 | { 323 | "name": "[variables('cognitiveServicesAllInOneName')]", 324 | "type": "Microsoft.CognitiveServices/accounts", 325 | "apiVersion": "2017-04-18", 326 | "sku": { 327 | "name": "S0" 328 | }, 329 | "kind": "CognitiveServices", 330 | "location": "[resourceGroup().location]", 331 | "properties": {} 332 | }, 333 | { 334 | "type": "Microsoft.Resources/deployments", 335 | "apiVersion": "2015-01-01", 336 | "name": "initaccelerator", 337 | "dependsOn": [ 338 | "[resourceId('Microsoft.Web/Sites/sourcecontrols', variables('functionAppName'), 'web')]" 339 | ], 340 | "properties": { 341 | "mode": "Incremental", 342 | "templateLink": { 343 | "uri": "[concat('https://raw.githubusercontent.com/', variables('repoDirectory'), variables('branch'),'/linkedazuredeploy.json')]", 344 | "contentVersion": "1.0.0.0" 345 | }, 346 | "parameters": { 347 | "functionAppName": { 348 | "value": "[variables('functionAppName')]" 349 | }, 350 | "siteName": { 351 | "value": "[variables('siteName')]" 352 | }, 353 | "hostingPlanName": { 354 | "value": "[variables('hostingPlanName')]" 355 | }, 356 | "reactProject": { 357 | "value": "[variables('reactProject')]" 358 | } 359 | } 360 | } 361 | } 362 | ], 363 | "outputs": { 364 | "HTTP trigger to initialize accelerator": { 365 | "type": "string", 366 | "value": "[reference('initaccelerator').outputs.URL.value]" 367 | }, 368 | "UI portal link": { 369 | "type": "string", 370 | "value": "[reference('initaccelerator').outputs.UI.value]" 371 | } 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /images/CogSearchQnAMakerArchitecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/images/CogSearchQnAMakerArchitecture.jpg -------------------------------------------------------------------------------- /images/deployment-original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/images/deployment-original.png -------------------------------------------------------------------------------- /images/deployment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/images/deployment.png -------------------------------------------------------------------------------- /images/initialize-accelerator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/images/initialize-accelerator.png -------------------------------------------------------------------------------- /images/qna-copy-url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/images/qna-copy-url.png -------------------------------------------------------------------------------- /images/search-results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/images/search-results.png -------------------------------------------------------------------------------- /images/web-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/search-qna-maker-accelerator/507fcbba23c1e9547d745756b41fd44a43abc21f/images/web-app.png -------------------------------------------------------------------------------- /linkedazuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "functionAppName": { 6 | "type": "string" 7 | }, 8 | "siteName": { 9 | "type": "string" 10 | }, 11 | "hostingPlanName": { 12 | "type": "string" 13 | }, 14 | "reactProject": { 15 | "type": "string" 16 | } 17 | }, 18 | "variables": { 19 | "zipUrl": "https://qnastoragei3iohrgwgujpo.blob.core.windows.net/code/web.zip" 20 | }, 21 | "resources": [ 22 | { 23 | "type": "Microsoft.Web/sites", 24 | "apiVersion": "2020-06-01", 25 | "name": "[parameters('siteName')]", 26 | "location": "[resourceGroup().location]", 27 | "dependsOn": [], 28 | "properties": { 29 | "serverFarmId": "[parameters('hostingPlanName')]", 30 | "siteConfig": { 31 | "appSettings": [ 32 | { 33 | "name": "REACT_APP_FUNCTION_URL", 34 | "value": "[concat('https://',parameters('functionAppName'),'.azurewebsites.net')]" 35 | }, 36 | { 37 | "name": "REACT_APP_FUNCTION_CODE", 38 | "value": "[listkeys(concat(resourceId('Microsoft.Web/sites', parameters('functionAppName')), '/host/default/'),'2019-08-01').functionKeys.default]" 39 | } 40 | ] 41 | } 42 | 43 | }, 44 | "resources": [ 45 | { 46 | "type": "Extensions", 47 | "apiVersion": "2015-02-01", 48 | "name": "MSDeploy", 49 | "dependsOn": [ 50 | "[concat('Microsoft.Web/Sites/', parameters('siteName'))]" 51 | ], 52 | "properties": { 53 | "packageUri": "[variables('zipUrl')]", 54 | "dbType": "None", 55 | "connectionString": "" 56 | } 57 | } 58 | ] 59 | } 60 | ], 61 | "outputs": { 62 | "URL": { 63 | "type": "string", 64 | "value": "[concat('https://',parameters('functionAppName'),'.azurewebsites.net/api/init-accelerator?code=',listkeys(concat(resourceId('Microsoft.Web/sites', parameters('functionAppName')), '/host/default/'),'2019-08-01').functionKeys.default)]" 65 | }, 66 | "UI": { 67 | "type": "string", 68 | "value": "[concat('https://',reference(resourceId('Microsoft.Web/sites', parameters('siteName'))).hostNames[0])]" 69 | } 70 | } 71 | } 72 | --------------------------------------------------------------------------------