From cfd0b2c28da1862848b26e0cef5e00e2573c1953 Mon Sep 17 00:00:00 2001
From: esikkala <esko.ikkala@aalto.fi>
Date: Mon, 26 Apr 2021 14:18:35 +0300
Subject: [PATCH] Refactored full text search

---
 package-lock.json                             | 220 +----------------
 package.json                                  |   2 +-
 src/client/actions/index.js                   |   8 +-
 .../MaterialTableFullTextResults.js           | 131 ----------
 .../facet_results/ReactVirtualizedTable.js    | 232 ++++++++++++++++++
 .../perspectives/sampo/FullTextSearch.js      |  20 +-
 src/client/containers/SemanticPortal.js       |   3 +
 src/client/reducers/sampo/fullTextSearch.js   |  48 +++-
 src/client/translations/sampo/localeEN.js     |  16 ++
 src/server/sparql/JenaQuery.js                |   4 +-
 src/server/sparql/SparqlQueriesGeneral.js     |  10 +
 .../sparql_queries/SparqlQueriesFullText.js   |  38 +--
 12 files changed, 356 insertions(+), 376 deletions(-)
 delete mode 100644 src/client/components/facet_results/MaterialTableFullTextResults.js
 create mode 100644 src/client/components/facet_results/ReactVirtualizedTable.js

diff --git a/package-lock.json b/package-lock.json
index 64f5dc9a..4cdadf08 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1338,16 +1338,6 @@
         "regenerator-runtime": "^0.13.4"
       }
     },
-    "@babel/runtime-corejs3": {
-      "version": "7.13.9",
-      "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.13.9.tgz",
-      "integrity": "sha512-p6WSr71+5u/VBf1KDS/Y4dK3ZwbV+DD6wQO3X2EbUVluEOiyXUk09DzcwSaUH4WomYXrEPC+i2rqzuthhZhOJw==",
-      "optional": true,
-      "requires": {
-        "core-js-pure": "^3.0.0",
-        "regenerator-runtime": "^0.13.4"
-      }
-    },
     "@babel/template": {
       "version": "7.12.13",
       "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz",
@@ -1431,14 +1421,6 @@
       "resolved": "https://registry.npmjs.org/@date-io/core/-/core-1.3.13.tgz",
       "integrity": "sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA=="
     },
-    "@date-io/date-fns": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-1.1.0.tgz",
-      "integrity": "sha512-FMRhYWfoGiIXdN4xWAArpkdEbqsg2Fr+6Yda7Np2eVWCNx6gSMYsHIM51IIcI+3762ajYbhoEYjHYXVFNZIk1g==",
-      "requires": {
-        "@date-io/core": "^1.1.0"
-      }
-    },
     "@date-io/moment": {
       "version": "1.3.13",
       "resolved": "https://registry.npmjs.org/@date-io/moment/-/moment-1.3.13.tgz",
@@ -6158,12 +6140,6 @@
       "integrity": "sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA==",
       "dev": true
     },
-    "@types/raf": {
-      "version": "3.4.0",
-      "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.0.tgz",
-      "integrity": "sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw==",
-      "optional": true
-    },
     "@types/reach__router": {
       "version": "1.3.7",
       "resolved": "https://registry.npmjs.org/@types/reach__router/-/reach__router-1.3.7.tgz",
@@ -7168,7 +7144,8 @@
     "atob": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
-      "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
+      "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
+      "dev": true
     },
     "autoprefixer": {
       "version": "9.8.6",
@@ -7845,12 +7822,6 @@
         }
       }
     },
-    "base64-arraybuffer": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz",
-      "integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==",
-      "optional": true
-    },
     "base64-js": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -8272,11 +8243,6 @@
         "node-int64": "^0.4.0"
       }
     },
-    "btoa": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
-      "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g=="
-    },
     "buffer": {
       "version": "4.9.2",
       "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
@@ -8471,20 +8437,6 @@
       "integrity": "sha512-8aE+sqBqtXz4G8g35Eg/XEaFr2N7rd/VQ6eABGBmNtcB8cN6qNJhMi6oSFy4UWWZgqgL3filHT8Nha4meu3tsw==",
       "dev": true
     },
-    "canvg": {
-      "version": "3.0.7",
-      "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.7.tgz",
-      "integrity": "sha512-4sq6iL5Q4VOXS3PL1BapiXIZItpxYyANVzsAKpTPS5oq4u3SKbGfUcbZh2gdLCQ3jWpG/y5wRkMlBBAJhXeiZA==",
-      "optional": true,
-      "requires": {
-        "@babel/runtime-corejs3": "^7.9.6",
-        "@types/raf": "^3.4.0",
-        "raf": "^3.4.1",
-        "rgbcolor": "^1.0.1",
-        "stackblur-canvas": "^2.0.0",
-        "svg-pathdata": "^5.0.5"
-      }
-    },
     "capture-exit": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz",
@@ -9533,7 +9485,8 @@
     "core-js-pure": {
       "version": "3.9.1",
       "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.9.1.tgz",
-      "integrity": "sha512-laz3Zx0avrw9a4QEIdmIblnVuJz8W51leY9iLThatCsFawWxC3sE4guASC78JbCin+DkwMpCdp1AVAuzL/GN7A=="
+      "integrity": "sha512-laz3Zx0avrw9a4QEIdmIblnVuJz8W51leY9iLThatCsFawWxC3sE4guASC78JbCin+DkwMpCdp1AVAuzL/GN7A==",
+      "dev": true
     },
     "core-util-is": {
       "version": "1.0.2",
@@ -9822,14 +9775,6 @@
         "randomfill": "^1.0.3"
       }
     },
-    "css-box-model": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
-      "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
-      "requires": {
-        "tiny-invariant": "^1.0.6"
-      }
-    },
     "css-in-js-utils": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz",
@@ -9839,15 +9784,6 @@
         "isobject": "^3.0.1"
       }
     },
-    "css-line-break": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-1.1.1.tgz",
-      "integrity": "sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==",
-      "optional": true,
-      "requires": {
-        "base64-arraybuffer": "^0.2.0"
-      }
-    },
     "css-loader": {
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.1.1.tgz",
@@ -10213,11 +10149,6 @@
         "node-addon-api": "^1.7.1"
       }
     },
-    "debounce": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz",
-      "integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg=="
-    },
     "debug": {
       "version": "2.6.9",
       "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -10669,12 +10600,6 @@
         "domelementtype": "1"
       }
     },
-    "dompurify": {
-      "version": "2.2.6",
-      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.2.6.tgz",
-      "integrity": "sha512-7b7ZArhhH0SP6W2R9cqK6RjaU82FZ2UPM7RO8qN1b1wyvC/NY1FNWcX1Pu00fFOAnzEORtwXe4bPaClg6pUybQ==",
-      "optional": true
-    },
     "domutils": {
       "version": "1.7.0",
       "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
@@ -12407,11 +12332,6 @@
       "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
       "optional": true
     },
-    "filefy": {
-      "version": "0.1.10",
-      "resolved": "https://registry.npmjs.org/filefy/-/filefy-0.1.10.tgz",
-      "integrity": "sha512-VgoRVOOY1WkTpWH+KBy8zcU1G7uQTVsXqhWEgzryB9A5hg2aqCyZ6aQ/5PSzlqM5+6cnVrX6oYV0XqD3HZSnmQ=="
-    },
     "filesize": {
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",
@@ -13600,15 +13520,6 @@
         }
       }
     },
-    "html2canvas": {
-      "version": "1.0.0-rc.7",
-      "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.0.0-rc.7.tgz",
-      "integrity": "sha512-yvPNZGejB2KOyKleZspjK/NruXVQuowu8NnV2HYG7gW7ytzl+umffbtUI62v2dCHQLDdsK6HIDtyJZ0W3neerA==",
-      "optional": true,
-      "requires": {
-        "css-line-break": "1.1.1"
-      }
-    },
     "htmlparser2": {
       "version": "3.10.1",
       "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
@@ -17630,24 +17541,6 @@
         "graceful-fs": "^4.1.6"
       }
     },
-    "jspdf": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.1.0.tgz",
-      "integrity": "sha512-NQygqZEKhSw+nExySJxB72Ge/027YEyIM450Vh/hgay/H9cgZNnkXXOQPRspe9EuCW4sq92zg8hpAXyyBdnaIQ==",
-      "requires": {
-        "atob": "^2.1.2",
-        "btoa": "^1.2.1",
-        "canvg": "^3.0.6",
-        "core-js": "^3.6.0",
-        "dompurify": "^2.0.12",
-        "html2canvas": "^1.0.0-rc.5"
-      }
-    },
-    "jspdf-autotable": {
-      "version": "3.5.9",
-      "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-3.5.9.tgz",
-      "integrity": "sha512-ZRfiI5P7leJuWmvC0jGVXu227m68C2Jfz1dkDckshmDYDeVFCGxwIBYdCUXJ8Eb2CyFQC2ok82fEWO+xRDovDQ=="
-    },
     "jsprim": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
@@ -18160,59 +18053,6 @@
       "integrity": "sha512-O8DMCl32V34RrD+ZHxcAPc2+kYytuDIoQYjY36RVdsLK7uHjgNVvFec4yv0X6LgB4YEZgSvK5QtFi5YVqEpoMA==",
       "dev": true
     },
-    "material-table": {
-      "version": "1.69.2",
-      "resolved": "https://registry.npmjs.org/material-table/-/material-table-1.69.2.tgz",
-      "integrity": "sha512-OFst2Dzo5EZHk5mTPVR9cILFT9BcDSVVlFii0UOgvWE8ho/sn0euV1B2MfoTLKSX6pMkH0I/f3QTLX0AXhlUzQ==",
-      "requires": {
-        "@date-io/date-fns": "1.1.0",
-        "@material-ui/pickers": "3.2.2",
-        "classnames": "2.2.6",
-        "date-fns": "2.0.0-alpha.27",
-        "debounce": "1.2.0",
-        "fast-deep-equal": "2.0.1",
-        "filefy": "0.1.10",
-        "jspdf": "2.1.0",
-        "jspdf-autotable": "3.5.9",
-        "prop-types": "15.6.2",
-        "react-beautiful-dnd": "13.0.0",
-        "react-double-scrollbar": "0.0.15"
-      },
-      "dependencies": {
-        "@material-ui/pickers": {
-          "version": "3.2.2",
-          "resolved": "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.2.2.tgz",
-          "integrity": "sha512-on/J1yyKeJ4CkLnItpf/jPDKMZVWvHDklkh5FS7wkZ0s1OPoqTsPubLWfA7eND6xREnVRyLFzVTlE3VlWYdQWw==",
-          "requires": {
-            "@babel/runtime": "^7.2.0",
-            "@types/styled-jsx": "^2.2.8",
-            "clsx": "^1.0.2",
-            "react-transition-group": "^4.0.0",
-            "rifm": "^0.7.0",
-            "tslib": "^1.9.3"
-          }
-        },
-        "date-fns": {
-          "version": "2.0.0-alpha.27",
-          "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.0.0-alpha.27.tgz",
-          "integrity": "sha512-cqfVLS+346P/Mpj2RpDrBv0P4p2zZhWWvfY5fuWrXNR/K38HaAGEkeOwb47hIpQP9Jr/TIxjZ2/sNMQwdXuGMg=="
-        },
-        "fast-deep-equal": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
-          "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
-        },
-        "prop-types": {
-          "version": "15.6.2",
-          "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
-          "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
-          "requires": {
-            "loose-envify": "^1.3.1",
-            "object-assign": "^4.1.1"
-          }
-        }
-      }
-    },
     "math.gl": {
       "version": "3.4.2",
       "resolved": "https://registry.npmjs.org/math.gl/-/math.gl-3.4.2.tgz",
@@ -18297,11 +18137,6 @@
         "fs-monkey": "1.0.3"
       }
     },
-    "memoize-one": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz",
-      "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA=="
-    },
     "memoizerific": {
       "version": "1.11.3",
       "resolved": "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz",
@@ -20579,11 +20414,6 @@
         "performance-now": "^2.1.0"
       }
     },
-    "raf-schd": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz",
-      "integrity": "sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ=="
-    },
     "railroad-diagrams": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz",
@@ -20674,20 +20504,6 @@
         "prop-types": "^15.6.2"
       }
     },
-    "react-beautiful-dnd": {
-      "version": "13.0.0",
-      "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz",
-      "integrity": "sha512-87It8sN0ineoC3nBW0SbQuTFXM6bUqM62uJGY4BtTf0yzPl8/3+bHMWkgIe0Z6m8e+gJgjWxefGRVfpE3VcdEg==",
-      "requires": {
-        "@babel/runtime": "^7.8.4",
-        "css-box-model": "^1.2.0",
-        "memoize-one": "^5.1.1",
-        "raf-schd": "^4.0.2",
-        "react-redux": "^7.1.1",
-        "redux": "^4.0.4",
-        "use-memo-one": "^1.1.1"
-      }
-    },
     "react-colorful": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.1.2.tgz",
@@ -21010,11 +20826,6 @@
         "scheduler": "^0.19.1"
       }
     },
-    "react-double-scrollbar": {
-      "version": "0.0.15",
-      "resolved": "https://registry.npmjs.org/react-double-scrollbar/-/react-double-scrollbar-0.0.15.tgz",
-      "integrity": "sha1-6RWrjLO5WYdwdfSUNt6/2wQoj+Q="
-    },
     "react-draggable": {
       "version": "4.4.3",
       "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.3.tgz",
@@ -22106,12 +21917,6 @@
       "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
       "dev": true
     },
-    "rgbcolor": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
-      "integrity": "sha1-1lBezbMEplldom+ktDMHMGd1lF0=",
-      "optional": true
-    },
     "rifm": {
       "version": "0.7.0",
       "resolved": "https://registry.npmjs.org/rifm/-/rifm-0.7.0.tgz",
@@ -23093,12 +22898,6 @@
         }
       }
     },
-    "stackblur-canvas": {
-      "version": "2.5.0",
-      "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.5.0.tgz",
-      "integrity": "sha512-EeNzTVfj+1In7aSLPKDD03F/ly4RxEuF/EX0YcOG0cKoPXs+SLZxDawQbexQDBzwROs4VKLWTOaZQlZkGBFEIQ==",
-      "optional": true
-    },
     "stackframe": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.0.tgz",
@@ -23608,12 +23407,6 @@
         }
       }
     },
-    "svg-pathdata": {
-      "version": "5.0.5",
-      "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-5.0.5.tgz",
-      "integrity": "sha512-TAAvLNSE3fEhyl/Da19JWfMAdhSXTYeviXsLSoDT1UM76ADj5ndwAPX1FKQEgB/gFMPavOy6tOqfalXKUiXrow==",
-      "optional": true
-    },
     "svg.draggable.js": {
       "version": "2.2.2",
       "resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
@@ -24712,11 +24505,6 @@
         "use-isomorphic-layout-effect": "^1.0.0"
       }
     },
-    "use-memo-one": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz",
-      "integrity": "sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ=="
-    },
     "util": {
       "version": "0.11.1",
       "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
diff --git a/package.json b/package.json
index c13b6913..d961048b 100644
--- a/package.json
+++ b/package.json
@@ -54,7 +54,6 @@
     "leaflet.markercluster": "^1.4.1",
     "leaflet.zoominfo": "git+https://github.com/SemanticComputing/Leaflet.zoominfo.git",
     "lodash": "^4.17.20",
-    "material-table": "^1.69.2",
     "moment": "^2.24.0",
     "moment-range": "^4.0.2",
     "react": "^16.13.0",
@@ -69,6 +68,7 @@
     "react-router-dom": "^5.2.0",
     "react-sortable-tree": "2.8.0",
     "react-sortable-tree-theme-file-explorer": "git+https://github.com/SemanticComputing/react-sortable-tree-theme-file-explorer.git",
+    "react-virtualized": "^9.22.3",
     "redux": "^4.0.1",
     "redux-observable": "^1.0.0",
     "reselect": "^4.0.0",
diff --git a/src/client/actions/index.js b/src/client/actions/index.js
index ca81c1ba..e9328f7d 100644
--- a/src/client/actions/index.js
+++ b/src/client/actions/index.js
@@ -5,6 +5,7 @@ export const FETCH_RESULTS_FAILED = 'FETCH_RESULTS_FAILED'
 export const FETCH_RESULT_COUNT = 'FETCH_RESULT_COUNT'
 export const FETCH_RESULT_COUNT_FAILED = 'FETCH_RESULT_COUNT_FAILED'
 export const FETCH_FULL_TEXT_RESULTS = 'FETCH_FULL_TEXT_RESULTS'
+export const SORT_FULL_TEXT_RESULTS = 'SORT_FULL_TEXT_RESULTS'
 export const UPDATE_RESULT_COUNT = 'UPDATE_RESULT_COUNT'
 export const UPDATE_PAGINATED_RESULTS = 'UPDATE_PAGINATED_RESULTS'
 export const UPDATE_RESULTS = 'UPDATE_RESULTS'
@@ -115,6 +116,11 @@ export const fetchFullTextResults = ({ resultClass, query }) => ({
   resultClass,
   query
 })
+export const sortFullTextResults = ({ resultClass, sortBy }) => ({
+  type: SORT_FULL_TEXT_RESULTS,
+  resultClass,
+  sortBy
+})
 export const fetchResultsFailed = (resultClass, error, message) => ({
   type: FETCH_RESULTS_FAILED,
   resultClass,
@@ -148,7 +154,7 @@ export const sortResults = (resultClass, sortBy) => ({
   resultClass,
   sortBy
 })
-export const clearResults = resultClass => ({
+export const clearResults = ({ resultClass }) => ({
   type: CLEAR_RESULTS,
   resultClass
 })
diff --git a/src/client/components/facet_results/MaterialTableFullTextResults.js b/src/client/components/facet_results/MaterialTableFullTextResults.js
deleted file mode 100644
index e0019eb5..00000000
--- a/src/client/components/facet_results/MaterialTableFullTextResults.js
+++ /dev/null
@@ -1,131 +0,0 @@
-import React from 'react'
-import PropTypes from 'prop-types'
-import { withStyles } from '@material-ui/core/styles'
-import MaterialTable from 'material-table'
-import SearchIcon from '@material-ui/icons/Search'
-import ClearIcon from '@material-ui/icons/Clear'
-import FirstPageIcon from '@material-ui/icons/FirstPage'
-import LastPageIcon from '@material-ui/icons/LastPage'
-import ChevronRightIcon from '@material-ui/icons/ChevronRight'
-import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'
-import CircularProgress from '@material-ui/core/CircularProgress'
-import ResultTableCell from './ResultTableCell'
-import purple from '@material-ui/core/colors/purple'
-import Paper from '@material-ui/core/Paper'
-
-const styles = () => ({
-  progressContainer: {
-    width: '100%',
-    height: 'calc(100% - 72px)',
-    display: 'flex',
-    alignItems: 'center',
-    justifyContent: 'center'
-  },
-  tableContainer: {
-    maxWidth: '100%',
-    height: 'calc(100% - 72px)'
-  }
-})
-
-class MaterialTableFullTextResults extends React.Component {
-  render () {
-    const results = this.props.data
-    const resultText = results === 1 ? 'result' : 'results'
-    if (this.props.fetching) {
-      return (
-        <Paper square className={this.props.classes.progressContainer}>
-          <CircularProgress style={{ color: purple[500] }} thickness={5} />
-        </Paper>
-      )
-    } else {
-      return (
-        <div className={this.props.classes.tableContainer}>
-          <MaterialTable
-            columns={[
-              {
-                title: 'Label',
-                field: 'prefLabel',
-                render: data =>
-                  <ResultTableCell
-                    columnId='prefLabel'
-                    data={data.prefLabel}
-                    valueType='object'
-                    makeLink
-                    externalLink={false}
-                    sortValues
-                    numberedList={false}
-                    minWidth={150}
-                    container='div'
-                    expanded
-                  />
-              },
-              {
-                title: 'Type',
-                field: 'type',
-                render: data =>
-                  <ResultTableCell
-                    columnId='type'
-                    data={data.type}
-                    valueType='object'
-                    makeLink={false}
-                    externalLink={false}
-                    sortValues
-                    numberedList={false}
-                    minWidth={150}
-                    container='div'
-                    expanded
-                  />
-              },
-              {
-                title: 'Source',
-                field: 'source',
-                render: data =>
-                  <ResultTableCell
-                    columnId='source'
-                    data={data.source}
-                    valueType='object'
-                    makeLink
-                    externalLink
-                    sortValues
-                    numberedList={false}
-                    minWidth={150}
-                    container='div'
-                    expanded
-                  />
-              }
-            ]}
-            data={results}
-            title={results > 1
-              ? `Search term: "${this.props.query}", ${results.length} ${resultText}`
-              : ''}
-            icons={{
-              Search: SearchIcon,
-              ResetSearch: ClearIcon,
-              FirstPage: FirstPageIcon,
-              LastPage: LastPageIcon,
-              NextPage: ChevronRightIcon,
-              PreviousPage: ChevronLeftIcon
-            }}
-            options={{
-              pageSize: 5,
-              pageSizeOptions: [5, 10, 15, 20, 25]
-            }}
-            style={{
-              height: '100%',
-              overflow: 'auto'
-            }}
-          />
-        </div>
-      )
-    }
-  }
-}
-
-MaterialTableFullTextResults.propTypes = {
-  classes: PropTypes.object.isRequired,
-  data: PropTypes.array,
-  query: PropTypes.string,
-  fetching: PropTypes.bool.isRequired
-}
-
-export default withStyles(styles)(MaterialTableFullTextResults)
diff --git a/src/client/components/facet_results/ReactVirtualizedTable.js b/src/client/components/facet_results/ReactVirtualizedTable.js
new file mode 100644
index 00000000..1591ff7c
--- /dev/null
+++ b/src/client/components/facet_results/ReactVirtualizedTable.js
@@ -0,0 +1,232 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import clsx from 'clsx'
+import { withStyles } from '@material-ui/core/styles'
+import TableCell from '@material-ui/core/TableCell'
+import TableSortLabel from '@material-ui/core/TableSortLabel'
+import Tooltip from '@material-ui/core/Tooltip'
+import ResultTableCell from './ResultTableCell'
+import Paper from '@material-ui/core/Paper'
+import { AutoSizer, Column, Table } from 'react-virtualized'
+import intl from 'react-intl-universal'
+import CircularProgress from '@material-ui/core/CircularProgress'
+import purple from '@material-ui/core/colors/purple'
+
+const styles = theme => ({
+  flexContainer: {
+    display: 'flex',
+    alignItems: 'center',
+    boxSizing: 'border-box'
+  },
+  table: {
+    // temporary right-to-left patch, waiting for
+    // https://github.com/bvaughn/react-virtualized/issues/454
+    '& .ReactVirtualized__Table__headerRow': {
+      flip: false,
+      paddingRight: theme.direction === 'rtl' ? '0 !important' : undefined
+    },
+    '& .ReactVirtualized__Table__rowColumn': {
+      marginRight: theme.spacing(3)
+    }
+  },
+  tableRow: {
+    borderBottom: '1px solid rgba(224, 224, 224, 1)'
+  },
+  tableRowHover: {
+    '&:hover': {
+      backgroundColor: theme.palette.grey[200]
+    }
+  },
+  tableCell: {
+    flex: 1
+  },
+  tableCellWithSort: {
+    marginRight: 16
+  }
+})
+
+class MuiVirtualizedTable extends React.PureComponent {
+  static defaultProps = {
+    headerHeight: 48,
+    rowHeight: 40
+  };
+
+  componentDidUpdate = prevProps => {
+    if (prevProps.sortBy !== this.props.sortBy ||
+      prevProps.sortDirection !== this.props.sortDirection) {
+      this.forceUpdate()
+    }
+  }
+
+  getRowClassName = ({ index }) => {
+    const { classes, onRowClick } = this.props
+
+    return clsx(classes.tableRow, classes.flexContainer, {
+      [classes.tableRowHover]: index !== -1 && onRowClick != null
+    })
+  };
+
+  cellRenderer = ({ cellData, columnIndex }) => {
+    const { columns /* classes, rowHeight, onRowClick */ } = this.props
+    const { id, valueType, makeLink, externalLink, sortValues, numberedList, minWidth } = columns[columnIndex]
+    return (
+      <ResultTableCell
+        columnId={id}
+        data={cellData}
+        valueType={valueType}
+        makeLink={makeLink}
+        externalLink={externalLink}
+        numberedList={numberedList}
+        sortValues={sortValues}
+        minWidth={minWidth}
+        container='div'
+        expanded
+      />
+    )
+  };
+
+  headerRenderer = ({ label, columnIndex, dataKey }) => {
+    const { headerHeight, columns, classes, sortBy, sortDirection } = this.props
+    return (
+      <>
+        <TableCell
+          component='div'
+          className={clsx(
+            classes.tableCell,
+            classes.flexContainer)}
+          variant='head'
+          style={{ height: headerHeight, textTransform: 'none' }}
+          align={columns[columnIndex].numeric || false ? 'right' : 'left'}
+        >
+          <Tooltip
+            title={`Sort by ${label}`}
+            enterDelay={300}
+          >
+            <TableSortLabel
+              active={sortBy === dataKey}
+              direction={sortDirection}
+              hideSortIcon
+              onClick={this.onSortBy(dataKey)}
+            >
+              {label}
+            </TableSortLabel>
+          </Tooltip>
+        </TableCell>
+      </>
+    )
+  }
+
+  onSortBy = sortBy => () => {
+    this.props.sortFullTextResults({
+      resultClass: 'fullText',
+      sortBy
+    })
+  }
+
+  render () {
+    const { classes, columns, rowHeight, headerHeight, sortDirection, ...tableProps } = this.props
+    return (
+      <AutoSizer>
+        {({ height, width }) => (
+          <Table
+            height={height}
+            width={width}
+            rowHeight={rowHeight}
+            gridStyle={{
+              direction: 'inherit'
+            }}
+            headerHeight={headerHeight}
+            className={classes.table}
+            {...tableProps}
+            rowClassName={this.getRowClassName}
+          >
+            {columns.map(({ id, minWidth, ...other }, index) => {
+              const label = intl.get(`perspectives.fullTextSearch.properties.${id}.label`)
+              return (
+                <Column
+                  key={id}
+                  headerRenderer={(headerProps) =>
+                    this.headerRenderer({
+                      ...headerProps,
+                      label,
+                      columnIndex: index,
+                      dataKey: id
+                    })}
+                  className={classes.flexContainer}
+                  cellRenderer={this.cellRenderer}
+                  dataKey={id}
+                  width={minWidth}
+                  {...other}
+                />
+              )
+            })}
+          </Table>
+        )}
+      </AutoSizer>
+    )
+  }
+}
+
+MuiVirtualizedTable.propTypes = {
+  classes: PropTypes.object.isRequired,
+  columns: PropTypes.arrayOf(
+    PropTypes.shape({
+      id: PropTypes.string.isRequired,
+      numeric: PropTypes.bool,
+      minWidth: PropTypes.number
+    })
+  ).isRequired,
+  headerHeight: PropTypes.number,
+  onRowClick: PropTypes.func,
+  rowHeight: PropTypes.number
+}
+
+const VirtualizedTable = withStyles(styles)(MuiVirtualizedTable)
+
+const rootStyle = {
+  height: 'calc(100% - 80px)',
+  fontFamily: 'Roboto'
+}
+
+const tableContainer = {
+  width: 700,
+  height: '100%',
+  marginLeft: 'auto',
+  marginRight: 'auto'
+}
+
+const progressContainerStyle = {
+  width: '100%',
+  height: 'calc(100% - 80px)',
+  display: 'flex',
+  alignItems: 'center',
+  justifyContent: 'center'
+}
+
+const ReactVirtualizedTable = props => {
+  const { results, properties, sortBy, sortDirection, fetching } = props.fullTextSearch
+  return (
+    <Paper square style={rootStyle}>
+      {fetching
+        ? (
+          <div style={progressContainerStyle}>
+            <CircularProgress style={{ color: purple[500] }} thickness={5} />
+          </div>
+        ) : (
+          <div style={tableContainer}>
+            <VirtualizedTable
+              rowCount={results.length}
+              rowGetter={({ index }) => results[index]}
+              columns={properties}
+              fetching={fetching}
+              sortBy={sortBy}
+              sortDirection={sortDirection}
+              sortFullTextResults={props.sortFullTextResults}
+            />
+          </div>
+        )}
+    </Paper>
+  )
+}
+
+export default ReactVirtualizedTable
diff --git a/src/client/components/perspectives/sampo/FullTextSearch.js b/src/client/components/perspectives/sampo/FullTextSearch.js
index 94cb82c5..b27a44a5 100644
--- a/src/client/components/perspectives/sampo/FullTextSearch.js
+++ b/src/client/components/perspectives/sampo/FullTextSearch.js
@@ -2,9 +2,16 @@ import React from 'react'
 import PropTypes from 'prop-types'
 import { Route, Redirect } from 'react-router-dom'
 import PerspectiveTabs from '../../main_layout/PerspectiveTabs'
-import MaterialTableFullTextResults from '../../facet_results/MaterialTableFullTextResults'
+import ReactVirtualizedTable from '../../facet_results/ReactVirtualizedTable'
 import CalendarViewDayIcon from '@material-ui/icons/CalendarViewDay'
 
+const rootStyle = {
+  height: 'calc(100% - 8px)',
+  marginTop: 8,
+  marginLeft: 8,
+  marginRight: 8
+}
+
 /**
  * A component for displaying full text search results.
  */
@@ -12,7 +19,7 @@ const FullTextSearch = props => {
   const { rootUrl } = props
   const perspectiveUrl = `${rootUrl}/full-text-search`
   return (
-    <>
+    <div style={rootStyle}>
       <PerspectiveTabs
         routeProps={props.routeProps}
         screenSize={props.screenSize}
@@ -31,15 +38,14 @@ const FullTextSearch = props => {
         path={`${perspectiveUrl}/table`}
         render={() => {
           return (
-            <MaterialTableFullTextResults
-              data={props.fullTextSearch.results || []}
-              query={props.fullTextSearch.query}
-              fetching={props.fullTextSearch.fetching}
+            <ReactVirtualizedTable
+              fullTextSearch={props.fullTextSearch}
+              sortFullTextResults={props.sortFullTextResults}
             />
           )
         }}
       />
-    </>
+    </div>
   )
 }
 
diff --git a/src/client/containers/SemanticPortal.js b/src/client/containers/SemanticPortal.js
index 1bd7ec0b..50bc1b93 100644
--- a/src/client/containers/SemanticPortal.js
+++ b/src/client/containers/SemanticPortal.js
@@ -42,6 +42,7 @@ import {
   fetchResults,
   fetchInstanceAnalysis,
   fetchFullTextResults,
+  sortFullTextResults,
   clearResults,
   fetchByURI,
   fetchFacet,
@@ -327,6 +328,7 @@ const SemanticPortal = props => {
                   <Grid item xs={12} className={classes.resultsContainer}>
                     <FullTextSearch
                       fullTextSearch={props.fullTextSearch}
+                      sortFullTextResults={props.sortFullTextResults}
                       routeProps={routeProps}
                       screenSize={screenSize}
                       rootUrl={rootUrlWithLang}
@@ -654,6 +656,7 @@ const mapDispatchToProps = ({
   fetchResults,
   fetchInstanceAnalysis,
   fetchFullTextResults,
+  sortFullTextResults,
   fetchByURI,
   fetchFacet,
   fetchFacetConstrainSelf,
diff --git a/src/client/reducers/sampo/fullTextSearch.js b/src/client/reducers/sampo/fullTextSearch.js
index c34dd1c3..e45cad18 100644
--- a/src/client/reducers/sampo/fullTextSearch.js
+++ b/src/client/reducers/sampo/fullTextSearch.js
@@ -1,12 +1,36 @@
 import {
   FETCH_FULL_TEXT_RESULTS,
+  SORT_FULL_TEXT_RESULTS,
   UPDATE_RESULTS,
   CLEAR_RESULTS
 } from '../../actions'
+import { orderBy } from 'lodash'
 
 export const INITIAL_STATE = {
   query: '',
-  results: null,
+  results: [],
+  sortBy: null,
+  sortDirection: null,
+  properties: [
+    {
+      id: 'prefLabel',
+      valueType: 'object',
+      makeLink: true,
+      externalLink: false,
+      sortValues: false,
+      numberedList: false,
+      minWidth: 400
+    },
+    {
+      id: 'type',
+      valueType: 'string',
+      makeLink: false,
+      externalLink: false,
+      sortValues: false,
+      numberedList: false,
+      minWidth: 300
+    }
+  ],
   fetching: false
 }
 
@@ -27,6 +51,28 @@ const fullTextSearch = (state = INITIAL_STATE, action) => {
         }
       case CLEAR_RESULTS:
         return INITIAL_STATE
+      case SORT_FULL_TEXT_RESULTS: {
+        let sortBy
+        let sortDirection
+        if (action.sortBy === state.sortBy) {
+          sortBy = state.sortBy
+          sortDirection = state.sortDirection === 'asc' ? 'desc' : 'asc'
+        }
+        if (action.sortBy !== state.sortBy) {
+          sortBy = action.sortBy
+          sortDirection = 'asc'
+        }
+        return {
+          ...state,
+          sortBy,
+          sortDirection,
+          results: orderBy(
+            state.results,
+            sortBy === 'prefLabel' ? 'prefLabel.prefLabel' : sortBy,
+            sortDirection
+          )
+        }
+      }
       default:
         return state
     }
diff --git a/src/client/translations/sampo/localeEN.js b/src/client/translations/sampo/localeEN.js
index 807788ec..81cb159d 100644
--- a/src/client/translations/sampo/localeEN.js
+++ b/src/client/translations/sampo/localeEN.js
@@ -667,6 +667,22 @@ export default {
         }
       }
     },
+    fullTextSearch: {
+      properties: {
+        prefLabel: {
+          label: 'Label',
+          description: ''
+        },
+        type: {
+          label: 'Type',
+          description: ''
+        },
+        source: {
+          label: 'Source',
+          description: ''
+        }
+      }
+    },
     manuscripts: {
       label: 'Manuscripts',
       facetResultsType: 'manuscripts',
diff --git a/src/server/sparql/JenaQuery.js b/src/server/sparql/JenaQuery.js
index 87eaa5f3..116d0398 100644
--- a/src/server/sparql/JenaQuery.js
+++ b/src/server/sparql/JenaQuery.js
@@ -1,6 +1,6 @@
 import { has } from 'lodash'
 import { runSelectQuery } from './SparqlApi'
-import { jenaQuery } from './SparqlQueriesGeneral'
+import { fullTextQuery } from './SparqlQueriesGeneral'
 import { makeObjectList } from './SparqlObjectMapper'
 
 export const queryJenaIndex = async ({
@@ -9,7 +9,7 @@ export const queryJenaIndex = async ({
   resultClass,
   resultFormat
 }) => {
-  let q = jenaQuery
+  let q = fullTextQuery
   const config = backendSearchConfig[resultClass]
   let endpoint
   if (has(config, 'endpoint')) {
diff --git a/src/server/sparql/SparqlQueriesGeneral.js b/src/server/sparql/SparqlQueriesGeneral.js
index f3e153f3..a1566dba 100644
--- a/src/server/sparql/SparqlQueriesGeneral.js
+++ b/src/server/sparql/SparqlQueriesGeneral.js
@@ -23,6 +23,16 @@ export const jenaQuery = `
   }
 `
 
+export const fullTextQuery = `
+  SELECT ?id ?prefLabel__id ?prefLabel__prefLabel ?prefLabel__dataProviderUrl 
+  (SAMPLE(?type_) as ?type) 
+  WHERE {
+    <QUERY>
+    <RESULT_SET_PROPERTIES>
+  }
+  GROUP BY ?id ?prefLabel__id ?prefLabel__prefLabel ?prefLabel__dataProviderUrl
+`
+
 export const facetResultSetQuery = `
   SELECT *
   WHERE {
diff --git a/src/server/sparql/sampo/sparql_queries/SparqlQueriesFullText.js b/src/server/sparql/sampo/sparql_queries/SparqlQueriesFullText.js
index c140a852..04e2513b 100644
--- a/src/server/sparql/sampo/sparql_queries/SparqlQueriesFullText.js
+++ b/src/server/sparql/sampo/sparql_queries/SparqlQueriesFullText.js
@@ -1,20 +1,23 @@
 export const fullTextSearchProperties = `
-{
-    ?id a ?type__id .
-    ?type__id rdfs:label|skos:prefLabel ?type__prefLabel_ .
-    BIND(STR(?type__prefLabel_) AS ?type__prefLabel)  # ignore language tags
-  }
-  UNION
-  {
-    ?id dct:source ?source__id .
-    ?source__id skos:prefLabel ?source__prefLabel .
-    ?source__id mmm-schema:data_provider_url ?source__dataProviderUrl .
-  }
-  UNION
   {
-    ?id mmm-schema:data_provider_url ?source__id .
-    BIND(?source__id as ?source__dataProviderUrl)
-    BIND(?source__id as ?source__prefLabel)
+    VALUES ?type_uri {
+      frbroo:F4_Manifestation_Singleton 
+      frbroo:F1_Work
+      frbroo:F2_Expression
+      crm:E10_Transfer_of_Custody
+      crm:E12_Production
+      crm:E7_Activity
+      crm:E67_Birth 
+      crm:E69_Death 
+      mmm-schema:ManuscriptActivity
+      crm:E21_Person 
+      crm:E74_Group 
+      crm:E39_Actor
+      crm:E53_Place
+      crm:E78_Collection
+    }
+    ?id a ?type_uri .
+    ?type_uri skos:prefLabel|rdfs:label ?type_ . 
   }
   UNION
   {
@@ -32,7 +35,7 @@ export const fullTextSearchProperties = `
   }
   UNION
   {
-    VALUES ?eventClass { crm:E10_Transfer_of_Custody crm:E12_Production crm:E7_Activity crm:E67_Birth crm:E69_Death }
+    VALUES ?eventClass { crm:E10_Transfer_of_Custody crm:E12_Production crm:E7_Activity crm:E67_Birth crm:E69_Death mmm-schema:ManuscriptActivity }
     ?id a ?eventClass .
     OPTIONAL { ?id skos:prefLabel ?prefLabel__id_ }
     BIND(COALESCE(?prefLabel__id_, ?id) as ?prefLabel__id)
@@ -50,7 +53,8 @@ export const fullTextSearchProperties = `
   UNION
   {
     ?id a crm:E53_Place .
-    ?id skos:prefLabel ?prefLabel__id .
+    OPTIONAL { ?id skos:prefLabel ?prefLabel__id_ }
+    BIND(COALESCE(?prefLabel__id_, ?id) as ?prefLabel__id)
     BIND(?prefLabel__id as ?prefLabel__prefLabel)
     BIND(CONCAT("/places/page/", REPLACE(STR(?id), "^.*\\\\/(.+)", "$1")) AS ?prefLabel__dataProviderUrl)
   }
-- 
GitLab